在 OpenMP 中如何指定不同 类 调用的函数之间的任务依赖关系?全局变量是唯一的解决方案吗?

In OpenMP how to specify task dependencies amongst functions invoked by different classes? Are global variables the only solution?

假设我们有两个 类:

class A
{
   void run_all()
   {
    
     #pragma omp task 
     f1a();

     #pragma omp task
     f1b();
   }
   
   void f1a()
   { /*some code*/ }


   void f1b()
   { /*some code*/ }
}

class B
{
   void run_all()
   {
     #pragma omp task 
     f2a();

     #pragma omp task
     f2b();
   }
   
   void f2a()
   { /*some code*/ }

   void f2b()
   { /*some code*/ }
}

然后我们有以下使用这些 类 的代码:

int main()
{
    
    A *a = new A();
    B *b = new B();
    #pragma omp parallel
    {
        #pragma omp single
        {
            a->run_all();
            b->run_all();
        }
    }
    
}

我需要确保四个子函数按以下顺序执行:f1a()、f1b()、f2a()、f2b()。为此,我需要使用带有 in/out/inout 子句的任务依赖项。换句话说,四个子函数需要看起来像这样:

   #pragma omp task depend (inout: x)
   fXa();

他们都使用一个公共变量 x。我如何定义这样的变量?它是否需要成为 C++ 中的全局变量,或者是否可以在不使用全局变量的情况下以其他方式完成?我是否需要定义一个虚拟变量 x 来定义任务依赖性,即使我不打算在其他任何地方使用该变量?在我看来,为了这样的目的需要定义一个变量有点奇怪......

顺便说一句:是的,我知道这可以通过删除所有 omp 编译指示来定义顺序问题来轻松完成,但这不是我正在寻找的答案,因为我有多个这样的子代码想运行并行

将全局变量用于此类用途可能不是一个好主意,因为如果 AB 存在。解决这个问题的一种方法是使用显式对象在 AB 之间共享依赖关系。此显式对象可以包含 OpenMP 为要序列化的任务所需的存储位置

这是生成的代码:

// Can contain multiple fields in the future in order to 
// support more complex synchronizations patterns.
struct DepHandler
{
    char seqTag;
};

class A
{
    public:

    A(DepHandler* dep) : m_dep(dep) { }

    void run_all()
    {
        #pragma omp task depend(inout: m_dep->seqTag)
        f1a();

        #pragma omp task depend(inout: m_dep->seqTag)
        f1b();
    }

    void f1a() { /*some code*/ }
    void f1b() { /*some code*/ }

    private:

    DepHandler* m_dep;
};

class B
{
    public:

    B(DepHandler* dep) : m_dep(dep) { }

    void run_all()
    {
        #pragma omp task depend(inout: m_dep->seqTag)
        f2a();

        #pragma omp task depend(inout: m_dep->seqTag)
        f2b();
    }

    void f2a() { /*some code*/ }
    void f2b() { /*some code*/ }

    private:

    DepHandler* m_dep;
};

int main()
{
    DepHandler dep;
    A a(&dep);
    B b(&dep);

    #pragma omp parallel
    {
        #pragma omp single
        {
            a.run_all();
            b.run_all();
        }
    }
}

这看起来有点麻烦,但实际上这样的做法有很多好处。事实上,即使 AB 不同的编译单元 中定义(这在面向对象的项目中通常是这种情况),该方法也确实有效。它们甚至可以定义在两个完全不同的项目 中,分别编译。将来可以添加更多序列化任务和更多标签(例如,如果可以将某些任务拆分为执行顺序不太严格的较小任务)。

事实上,我认为如果任务需要串行执行,这很可能是因为它们共享某些东西(隐式数据)。有时可以使用依赖关系来约束调度(例如,当任务需要大量内存并且不能同时执行时),但即使在这种情况下,使用显式依赖对象也有助于维护代码。


补充说明

这个依赖变量实际上对 OpenMP 运行时很有用。运行时可以使用所提供变量的指针来有效地检索相关任务。运行时甚至不需要分配任何与任务依赖相关的内部对象,也不需要控制这些对象的生命周期。负担留给程序员,程序员通常可以比运行时更有效地做到这一点。

请注意,一些高级 OpenMP 运行时可以跟踪存储位置指向任务依赖性的位置,以便选择在哪个位置安排任务。例如,在彼此靠近的地方执行在同一存储位置上工作的任务通常会更有效率一些(以改善 locality)。因此,提供有关任务处理的实际数据可能有助于获得更好的调度(例如,在 NUMA 感知 OpenMP 运行时)。

请注意,如果您希望任务按顺序执行但不关心顺序,则依赖类型 mutexinoutset 可能很有用。