OMP critical和OMP single的区别

55
我正在尝试理解OpenMP中#pragma omp critical#pragma omp single之间的确切区别。Microsoft对这些的定义是:
  • Single:让您指定一段代码应该在单个线程上执行,不一定是主线程。
  • Critical:指定代码只能一次在一个线程上执行。
因此,这意味着在两者中,之后的确切代码部分将仅由一个线程执行,而其他线程将不会进入该部分。例如,如果我们打印一些东西,我们将只看到屏幕上的结果一次,对吗?它们之间的区别如何?它看起来critical关注执行时间,但single没有!但在实践中我看不出任何区别!这是否意味着critical考虑了其他线程(不进入该部分)的等待或同步,但single没有?它如何在实践中改变结果?如果有人能通过例子向我澄清这一点,我将不胜感激。谢谢!
2个回答

123

singlecritical是两个非常不同的概念。正如您所提到的:

  • single指定一段代码应该由单个线程(不一定是主线程)执行
  • critical指定代码一次只能由一个线程执行

因此,前者将仅被执行一次,而后者将根据线程数量执行多次

例如以下代码:

int a=0, b=0;
#pragma omp parallel num_threads(4)
{
    #pragma omp single
    a++;
    #pragma omp critical
    b++;
}
printf("single: %d -- critical: %d\n", a, b);

将打印

single: 1 -- critical: 4

我希望你现在能更好地看到它们之间的区别。

为了完整起见,我可以补充一下:

  • mastersingle 非常相似,但有两个不同之处:
    1. master 只由主线程执行,而 single 可以被任何到达该区域的线程执行;
    2. single 在完成区域时有一个隐式的屏障,所有线程都等待同步,而 master 没有任何屏障。
  • atomiccritical 非常相似,但仅限于一些简单操作。

我添加这些说明是因为人们经常会混淆这两组指令...


非常感谢。我明白了!那么我们可以说:critical是一种避免竞态条件的屏障,而single适用于只需执行一次的轻型任务?如果我们将critical放在一个if语句中,只允许一个特定的线程进入,那么它是否与single类似? - Amir
是的,critical 是为了避免竞态条件。它的典型用法是在变量为数组时替换 reduction() 子句,因为在 C 和 C++ 中,reduction 子句仅适用于标量变量。至于在 if 语句中放置 critical,我真的看不出有什么意义... - Gilles
我认为OP只是想知道它是否等效(如果对于一个带有critical vs. single的线程)。许多OpenMP子句可以用其他子句模拟。我认为找到模拟所有或大多数子句的最小子句集会很有趣。知道这样的事情有时很有用。在某些情况下,OpenMP子句过于限制,知道如何自己完成(而无需使用其他线程接口,如pthreads)是有用的。我能想到的最常见的例子是进行自定义缩减。 - Z boson

44

singlecritical 属于不同类别的 OpenMP 结构。 single 是一种工作分享结构,与 forsections 并列。 工作分享结构用于在线程之间分配一定量的工作。这些结构在正确的 OpenMP 程序中“集体”存在,意思是所有线程必须在执行时遇到它们,并且按照相同的顺序,包括 barrier 结构。 这三个工作分享结构涵盖三种不同的一般情况:

  • for(也称为循环结构)自动在线程之间分配一个循环的迭代-在大多数情况下,所有线程都有工作要做;
  • sections 在线程之间分配一系列独立的代码块-某些线程需要工作。这是将具有 100 个迭代的循环表示为例如 10 个每个具有 10 个迭代的循环的部分的概括。
  • single 将代码块从中提出来仅由一个线程执行,通常是第一个遇到它的线程(实现细节)-仅有一个线程可以获得工作。 single 在很大程度上等同于只有一个部分的 sections

所有工作分享结构的共同特点是在其结尾处存在隐式屏障,该屏障可能通过将 nowait 子句添加到相应的 OpenMP 结构来关闭,但标准不要求此行为,并且对于某些 OpenMP 运行时,即使存在 nowait,屏障仍可能继续存在。因此,错误排序(例如在某些线程中顺序错误)的工作分享结构可能导致死锁。当存在障碍时,正确的 OpenMP 程序永远不会死锁。

critical 是一种同步结构,与 masteratomic 和其他结构并列使用。同步结构用于防止竞争条件并在执行过程中带来顺序。

  • critical通过阻止所谓的竞争组中的线程同时执行代码,避免了竞态条件。这意味着遇到类似命名的关键构造的所有并行区域中的所有线程都会被串行化;
  • atomic将某些简单的内存操作转换为原子操作,通常是通过利用特殊的汇编指令来实现的。原子操作作为一个单一的不可打断的单元立即完成。例如,一个线程从某个位置进行原子读取,而另一个线程同时对同一位置进行原子写入,则返回的要么是旧值,要么是更新后的值,但永远不会是旧值和新值的某种中间混合体;
  • master将一段代码块单独指定为仅由主线程(ID为0的线程)执行。与single不同,此构造函数结束时没有隐式屏障,并且也没有要求所有线程必须遇到master构造函数。此外,缺少隐式屏障意味着master不会刷新线程的共享内存视图(这是OpenMP中非常重要但很难理解的部分)。master基本上是一个简写形式的if (omp_get_thread_num() == 0) { ... }

critical是非常通用的构造函数,因为它能够在程序代码的不同部分甚至不同的并行区域中将不同的代码串行化(仅在嵌套并行性的情况下有显著作用)。每个critical构造都有一个可选的名称,紧随其后括号内即可。匿名的关键构造共享相同的实现特定名称。一旦线程进入这样的构造函数,任何遇到另一个具有相同名称的构造函数的线程都会被放置在等待状态,直到原始线程退出其构造函数。然后,序列化过程将继续进行剩余的线程。

以下是上述概念的示例代码:

#pragma omp parallel num_threads(3)
{
   foo();
   bar();
   ...
}

结果类似于:

thread 0: -----< foo() >< bar() >-------------->
thread 1: ---< foo() >< bar() >---------------->
thread 2: -------------< foo() >< bar() >------>

(线程2故意晚来)

foo()的调用置于single结构中:

#pragma omp parallel num_threads(3)
{
   #pragma omp single
   foo();
   bar();
   ...
}

结果类似于:

thread 0: ------[-------|]< bar() >----->
thread 1: ---[< foo() >-|]< bar() >----->
thread 2: -------------[|]< bar() >----->

这里[...]表示single结构的作用域,|是其隐含的屏障。请注意后来的线程2使所有其他线程等待。线程1执行foo()调用作为示例OpenMP运行时选择将工作分配给首次遇到该结构的线程。

添加nowait从句可能会移除隐含的屏障,产生类似以下的结果:

thread 0: ------[]< bar() >----------->
thread 1: ---[< foo() >]< bar() >----->
thread 2: -------------[]< bar() >---->

在匿名的 critical 结构中调用 foo();

#pragma omp parallel num_threads(3)
{
   #pragma omp critical
   foo();
   bar();
   ...
}

导致结果类似于:

thread 0: ------xxxxxxxx[< foo() >]< bar() >-------------->
thread 1: ---[< foo() >]< bar() >------------------------->
thread 2: -------------xxxxxxxxxxxx[< foo() >]< bar() >--->

使用xxxxx...显示了一个线程在进入其自身构造之前等待执行同名关键结构的其他线程所花费的时间。

不同名称的关键结构不会相互同步。例如:

#pragma omp parallel num_threads(3)
{
   if (omp_get_thread_num() > 1) {
     #pragma omp critical(foo2)
     foo();
   }
   else {
     #pragma omp critical(foo01)
     foo();
   }
   bar();
   ...
}

导致类似这样的结果:

thread 0: ------xxxxxxxx[< foo() >]< bar() >---->
thread 1: ---[< foo() >]< bar() >--------------->
thread 2: -------------[< foo() >]< bar() >----->

现在线程2不与其他线程同步,因为它的关键构造被命名为不同的名称,因此可能会对foo()进行危险的同时调用。

另一方面,匿名关键构造体(以及通常具有相同名称的构造体)无论它们在代码中的位置如何,都会相互同步:

#pragma omp parallel num_threads(3)
{
   #pragma omp critical
   foo();
   ...
   #pragma omp critical
   bar();
   ...
}

以及最终的执行时间轴:

thread 0: ------xxxxxxxx[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]------------>
thread 1: ---[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]----------------------->
thread 2: -------------xxxxxxxxxxxx[< foo() >]< ... >xxxxxxxxxxxxxxx[< bar() >]->

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接