omp critical 和 omp single 之间的区别

difference between omp critical and omp single

我想了解 OpenMP 中 #pragma omp critical#pragma omp single 之间的确切区别:

Microsoft 对这些的定义是:

所以这意味着在这两种情况下,之后的确切代码部分将仅由一个线程执行,而其他线程不会进入该部分,例如如果我们打印一些东西,我们会在屏幕上看到一次结果,对吗?

有什么区别?看起来 critical 照顾执行时间,但不是单一的!但我没有看到实践中的任何区别!这是否意味着其他线程(不进入该部分)的一种等待或同步被认为是关键的,但没有任何东西可以将其他线程保持在单一状态?它如何在实践中改变结果?

如果有人能通过示例向我说明这一点,我将不胜感激。谢谢!

singlecritical 是两个非常不同的东西。正如您提到的:

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

因此前者将仅执行一次,而后者将执行与线程一样多的次数[=]。 24=]

例如下面的代码

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 只会被 master 执行,而 single 可以被先到达该区域的线程执行;和
    2. single 在区域完成时有一个隐式屏障,所有线程都在等待同步,而 master 没有。
  • atomiccritical非常相似,但仅限于选择简单的操作。

我添加了这些精度,因为人们往往会混淆这两对指令...

singlecritical属于两个完全不同的类OpenMP结构。 single 是一个工作共享结构,与 forsections 并列。工作共享结构用于在线程之间分配一定数量的工作。这样的构造是 "collective",因为在正确的 OpenMP 程序中,所有线程 必须 在执行时遇到它们,而且以相同的顺序排列,也包括 barrier 构造.三种工作共享结构涵盖三种不同的一般情况:

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

所有工作共享结构的一个共同特征是在它们的末端存在一个隐式障碍,这个障碍 可能 通过将 nowait 子句添加到相应的 OpenMP 构造,但标准不要求此类行为,并且对于某些 OpenMP 运行时,尽管 nowait 存在,但障碍可能会继续存在。排序不正确(即在某些线程中顺序不正确)的工作共享结构可能因此导致死锁。当存在障碍时,正确的 OpenMP 程序永远不会死锁。

critical 是一个同步结构,与 masteratomic 等并列。同步结构用于防止竞争条件并在事物的执行中带来秩序。

  • critical 通过阻止在所谓的 竞争组 中的线程之间同时执行代码来防止竞争条件。这意味着来自 all 个并行区域的 all 个线程遇到类似命名的关键构造时会被序列化;
  • atomic 将某些简单的内存操作变成原子操作,通常是通过使用特殊的汇编指令。原子作为一个不可破坏的单元立即完成。例如,一个线程从某个位置的原子读取,与另一个线程对同一位置的原子写入同时发生,将 return 旧值或更新值,但永远不会是某种中间值来自旧值和新值的位混搭;
  • master挑出一段代码,仅供主线程(ID为0的线程)执行。与 single 不同,构造末尾没有隐式屏障,也没有要求所有线程都必须遇到 master 构造。此外,缺少隐式屏障意味着 master 不会刷新线程的共享内存视图(这是 OpenMP 的一个重要但知之甚少的部分)。 master 基本上是 if (omp_get_thread_num() == 0) { ... } 的 shorthand。

critical 是一个非常通用的结构,因为它能够在程序代码的非常不同的部分序列化不同的代码片段,甚至在不同的并行区域(仅在嵌套并行的情况下很重要)。每个 critical 结构都有一个可选名称,紧跟在后面的括号中。匿名关键构造共享相同的特定于实现的名称。一旦一个线程进入这样的构造,任何其他遇到另一个同名构造的线程都会被搁置,直到原始线程退出它的构造。然后序列化过程继续处理其余线程。

上述概念的说明如下。以下代码:

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

结果类似于:

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

(线程2故意迟到)

single 结构中进行 foo(); 调用:

#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() >]->