OpenMP:不能同时使用 omp parallel for 和 omp task 吗? /错误:工作共享区域可能没有紧密嵌套在工作共享内

Posted

技术标签:

【中文标题】OpenMP:不能同时使用 omp parallel for 和 omp task 吗? /错误:工作共享区域可能没有紧密嵌套在工作共享内【英文标题】:OpenMP: is it not possibe to use omp parallel for and omp task together? /error: work-sharing region may not be closely nested inside of work-sharing 【发布时间】:2022-01-10 02:19:05 【问题描述】:

在我的 c++ 实现中,我有一个递归函数,我在循环中调用这个递归函数。我想使用 OpenMP 创建并行性。

func caller()

  #pragma omp parallel    
    
    #pragma omp for nowait    
    for (int i = 0; i < num; i++)
        #pragma omp single
        recursive_func(n);   
    
  

void recursive_func(n)

  if (x)
    #pragma omp task
    recursive_func(n-1);   
     
  else
    #pragma omp task
    recursive_func(n-2);  
  

我基本上希望外循环由多个线程执行,但是当涉及到递归函数时,只有一个线程应该开始执行它,然后递归调用应该由新任务处理(这将与 @ 987654323@)

但是,我收到此编译错误:

错误:工作共享区域可能没有紧密嵌套在 工作共享、“关键”、“有序”、“大师”、明确的“任务”或 任务循环区域 68 | #pragma omp single

【问题讨论】:

1.我想知道nowait 是否是问题2。在循环之前执行主体single. How about if you put the single 并且不使其并行的并行循环没有多大意义? @VictorEijkhout 是的,我明白你的意思,单曲破坏了平行的目的。我只是想让许多线程同时执行递归函数,并且每次都由不同的任务执行递归函数 @codertryer 如果您想在工作共享结构中创建新任务,您必须在递归函数中打开一个新的omp parallel 部分。但是,我建议不要这样做,因为它只会使 CPU 过载,并且 OpenMP 将始终启动新线程,而不是将其线程池用于嵌套并行部分。你真正想解决什么问题? @Homer512 我同意在嵌套情况下创建新线程(尽管我记得这可以针对 GCC/Clang 进行调整)。然而,这里没有(需要)嵌套:任务可以被调度并行for循环,这不是嵌套。在这种情况下,并行部分创建隐式任务(parallel for 指令仅共享工作),并且每个隐式任务的同级任务在设计上是独立的(尽管在这里这是一个问题,因为 OP 希望它们以互斥)。这在 OpenMP 5.2 规范的第 1.3、17.1 和 15.9.5 节中有详细说明。 @JérômeRichard 好吧,在这种情况下你似乎不需要嵌套是正确的。作为记录,在第 429 行 code.woboq.org/gcc/libgomp/team.c.html 中解释了嵌套线程缺乏池化 【参考方案1】:

您不能将 #pragma omp single 指令放在 #pragma omp for 区域中,因为 for 的工作已经在线程之间共享,因此该指令已经保证只能由一个线程执行这种情况(更不用说 single 指令最终会造成障碍,除非您使用关闭 nowait)。

如果您不希望两个 recursive_func 调用不被并行调用,那么您需要一个(隐式/显式)同步

一种(相当糟糕的)方法是使用临界区 (#pragma omp critical)。临界区中的任务可能被其他线程窃取/影响。这只是可能的一些线程到达一个调度点。如果没有这样的线程,那么任务将在当前线程上串行执行。并行区域的末端是一个调度点(如屏障、任务循环、任务等待等)。因此,线程可能仅在执行并行循环(至少其专用部分)之后才执行递归函数的任务。更糟糕的是:关键部分不是调度点,因此其他线程可能最终会在有一些工作要执行时什么都不等待。您不能强制 OpenMP 停止执行并行循环的其他线程,除非您添加一些额外的同步来限制其他线程的执行。

另一种(相当糟糕的)解决方案是在所有递归任务都已安排好之后设置一个标志,以便其他线程在看到标志设置时停止工作。要强制线程暂时停止执行主循环,直到所有任务都已执行,您可以在循环中使用#pragma omp taskyield,该循环在原子值达到特定值时进行迭代(说明是否所有递归任务都已执行) .但是,OpenMP 实现可能不会在这样的线程中执行任务,从而导致非常糟糕的调度。事实上,在taskyield 中什么都不做完全符合标准(实际上许多主流的 OpenMP 实现过去都没有实现它)。此外,这种方法补充起来很复杂。请注意,barrier 在这种情况下不能使用,taskwait 并不比 taskyield 好多少,因为计划任务是主并行循环的隐式任务的子任务。

一般来说,并行 for 循环和任务不能很好地结合在一起。此外,尝试使用任务和同步原语强制给定调度是很棘手的,即使由于对目标 OpenMP 实现所做的隐式假设没有缺陷。在复杂的情况下,使用平面任务模型有时可以提供比分层模型更大的灵活性。

更好的解决方案是将每次迭代拆分为 3 个任务并为每次迭代创建任务(它们可以手动合并,尽管这有点棘手),然后告诉 OpenMP 递归任务比其他任务“更重要”为此类任务设置更高的优先级。此外,如果您不希望对recursive_func 的多次调用并行执行,您可以使用互斥任务依赖项。最后,同一迭代的任务共享相同的inout依赖变量,从而顺序执行。

这是一个 (OpenMP 5) 示例:

void caller(int num)

    #pragma omp parallel    
    #pragma omp single
    
        char* deps = new char[num];
        char criticalDep;

        for (int i = 0; i < num; i++)
        
            #pragma omp task depend(out: deps[i]) priority(0)
            someWork1(i);

            #pragma omp task depend(inout: deps[i]) \
                             depend(mutexinoutset: criticalDep) \
                             priority(1)
            recursive_func(i);

            #pragma omp task depend(in: deps[i]) priority(0)
            someWork2(i);
        

        delete[] deps;
    


void recursive_func(int n) 
  if (x)
    #pragma omp task priority(1)
    recursive_func(n-1);   
     
  else
    #pragma omp task priority(1)
    recursive_func(n-2);
  

但是,请注意:

兼容的 OpenMP 实现可以完全忽略任务优先级(但必须遵循依赖关系); 应仔细调整任务的粒度,因为此解决方案虽然更灵活,但可以创建比其他解决方案更多的任务; 实现可能会以不希望的顺序执行任务,因此您可能需要调整优先级。

【讨论】:

以上是关于OpenMP:不能同时使用 omp parallel for 和 omp task 吗? /错误:工作共享区域可能没有紧密嵌套在工作共享内的主要内容,如果未能解决你的问题,请参考以下文章

使用 omp_set_num_threads 后,我可以让 OpenMP 恢复到理想的线程数吗?

OpenMP #pragma omp for v/s #pragma omp parallel for 之间的区别?

在 OpenMP 中,我们如何并行运行多个代码块,其中每个代码块包含 omp single 和 omp for 循环?

并行程序设计导论学习笔记——OpenMP

Openmp编程练习

通过分离#omp parallel 和#omp for 来减少 OpenMP fork/join 开销