#pragma omp critical
和#pragma omp single
之间的确切区别。Microsoft对这些的定义是:- Single:让您指定一段代码应该在单个线程上执行,不一定是主线程。
- Critical:指定代码只能一次在一个线程上执行。
#pragma omp critical
和#pragma omp single
之间的确切区别。Microsoft对这些的定义是:single
和critical
是两个非常不同的概念。正如您所提到的:
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
我希望你现在能更好地看到它们之间的区别。
为了完整起见,我可以补充一下:
master
与 single
非常相似,但有两个不同之处:
master
只由主线程执行,而 single
可以被任何到达该区域的线程执行;single
在完成区域时有一个隐式的屏障,所有线程都等待同步,而 master
没有任何屏障。atomic
与 critical
非常相似,但仅限于一些简单操作。我添加这些说明是因为人们经常会混淆这两组指令...
single
和 critical
属于不同类别的 OpenMP 结构。 single
是一种工作分享结构,与 for
和 sections
并列。 工作分享结构用于在线程之间分配一定量的工作。这些结构在正确的 OpenMP 程序中“集体”存在,意思是所有线程必须在执行时遇到它们,并且按照相同的顺序,包括 barrier
结构。 这三个工作分享结构涵盖三种不同的一般情况:
for
(也称为循环结构)自动在线程之间分配一个循环的迭代-在大多数情况下,所有线程都有工作要做;sections
在线程之间分配一系列独立的代码块-某些线程需要工作。这是将具有 100 个迭代的循环表示为例如 10 个每个具有 10 个迭代的循环的部分的概括。single
将代码块从中提出来仅由一个线程执行,通常是第一个遇到它的线程(实现细节)-仅有一个线程可以获得工作。 single
在很大程度上等同于只有一个部分的 sections
。所有工作分享结构的共同特点是在其结尾处存在隐式屏障,该屏障可能通过将 nowait
子句添加到相应的 OpenMP 结构来关闭,但标准不要求此行为,并且对于某些 OpenMP 运行时,即使存在 nowait
,屏障仍可能继续存在。因此,错误排序(例如在某些线程中顺序错误)的工作分享结构可能导致死锁。当存在障碍时,正确的 OpenMP 程序永远不会死锁。
critical
是一种同步结构,与 master
、atomic
和其他结构并列使用。同步结构用于防止竞争条件并在执行过程中带来顺序。
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() >]->
critical
是为了避免竞态条件。它的典型用法是在变量为数组时替换reduction()
子句,因为在 C 和 C++ 中,reduction
子句仅适用于标量变量。至于在 if 语句中放置critical
,我真的看不出有什么意义... - Gilles