OpenMP中的atomic和critical有什么区别?

127
OpenMP中atomic和critical的区别是什么?
我能做到。
#pragma omp atomic
g_qCount++;

这不就跟

#pragma omp critical
g_qCount++;

?

8个回答

202

对于 g_qCount 的影响是相同的,但执行方式不同。

OpenMP 的临界区是完全通用的 - 它可以包围任意一段代码。然而,您需要为此付出代价,每次线程进入和退出临界区时都会产生显着的开销(除了串行化本身的成本之外)。

(此外,在 OpenMP 中,所有未命名的临界区都被认为是相同的(如果您愿意,所有未命名的临界区只有一个锁),因此如果一个线程在上面的一个 [未命名的] 临界区中,则没有线程能进入任何 [未命名的] 临界区。如您猜测的那样,您可以通过使用命名的临界区来解决这个问题)。

原子操作的开销要小得多。如果可用,它利用提供原子增量操作的硬件;在这种情况下,在输入/输出代码行时不需要锁定/解锁,它只执行硬件告诉你不能干扰的原子增量操作。

优点是开销更小,一个线程在原子操作中不会阻塞即将发生的任何(不同的)原子操作。缺点是原子支持的操作集受到限制。

当然,在任一情况下,您都需要承担串行化的成本。


7
“you could loose portability” - 我不确定这是正确的。标准(版本2.0)指定了哪些原子操作是允许的(基本上像++*=这样的操作),并且如果硬件不支持它们,则可能会被“critical”部分替代。 - Dan R
@DanRoche:是的,你说得很对。我认为那个陈述从来都不正确,我现在会进行更正。 - Jonathan Dursi
几天前,我跟随一个OpenMP教程学习,据我所知,两种不同的代码有所区别。这是因为关键部分确保指令只由一个线程执行,但是对于指令:g_qCount = g_qCount+1;对于线程1,它可能只将g_qCount结果存储在写缓冲区中而不是RAM内存中,当线程2获取g_qCount值时,它只读取RAM中的值,而不是写缓冲区中的值。原子指令确保指令刷新数据到内存。 - Giox79

35

在OpenMP中,所有未命名的关键段都是互斥的。

critical和atomic之间最重要的区别是,atomic只能保护单个赋值,并且您可以使用特定操作符。


15
这更适合作为对前面答案的评论(或编辑)。 - kynan

32

临界区:

  • 确保代码块的串行化。
  • 使用“name”标签可以将多个代码块串行化。

  • 速度较慢!

原子操作:

  • 速度更快!

  • 仅保证特定操作的串行化。


12
但这个回答很易读,将是第一个回答的很好总结。 - Michał Miszczyszyn

7

最快的方法既不是关键的也不是原子的。大约来说,带关键段的加法比简单加法贵200倍,原子加法比简单加法贵25倍。

最快的选项(并非总是适用)是给每个线程分配一个计数器,并在需要总和时进行减少操作。


2
我不同意你在解释中提到的所有数字。假设是x86_64,原子操作将产生一些周期开销(同步高速缓存行)成本大约为一个周期。如果您有其他“真实共享”成本,则开销为零。关键部分会产生锁定成本。根据锁是否已被占用,开销大约为2个原子指令或两次调度程序和睡眠时间,通常这比200倍还要多。 - Klaas van Gend
您提出的选项可能会导致我们无法使用大量内存。例如,如果我正在处理1000x1000x1000个单元格的数据,并且使用10或100个线程进行工作,则为每个线程创建的内部副本肯定会使RAM饱和。 - Noureddine

6

atomic的局限性非常重要。它们应该在OpenMP规范中详细说明。由于Visual Studio 2012自2002年3月以来已经实现了OpenMP,因此MSDN提供了一个快速的备忘单,我不会感到惊讶如果这将不会改变。

引用MSDN:

表达式语句必须采用以下形式之一:

xbinop=expr

x++

++x

x--

--x

在上述表达式中:x是具有标量类型的表达式。 expr是具有标量类型的表达式,并且不引用x所指定的对象。binop不是重载运算符,而是+*-/&^|<<>>之一。

我建议在可以使用atomic的情况下使用它,在其他情况下使用命名关键部分。对其进行命名非常重要;这样您就可以避免调试方面的麻烦。


1
这不是全部,我们还有其他高级原子指令,例如: #pragma omp aromic update (或 read、upate、write、capture),因此它允许我们拥有其他有益的语句。 - pooria

1

这里已经有很好的解释了。但是,我们可以深入一点。为了理解OpenMP中原子临界区概念之间的核心区别,我们必须先了解的概念。让我们回顾一下为什么需要使用

并行程序由多个线程执行。只有在这些线程之间执行同步时才会产生确定性结果。当然,并不总是需要在线程之间进行同步。我们指的是那些必须进行同步的情况。

为了在多线程程序中同步线程,我们将使用。当需要限制只有一个线程访问时,就要用到。从算法的角度来看,的实现可能因处理器而异。让我们找出一个简单的锁如何工作。

1. Define a variable called lock.
2. For each thread:
   2.1. Read the lock.
   2.2. If lock == 0, lock = 1 and goto 3    // Try to grab the lock
       Else goto 2.1    // Wait until the lock is released
3. Do something...
4. lock = 0    // Release the lock

给定的算法可以如下实现硬件语言。我们将假设使用单个处理器并分析锁的行为。对于这个练习,让我们假设以下其中一个处理器:MIPSAlphaARMPower
try:    LW R1, lock
        BNEZ R1, try
        ADDI R1, R1, #1
        SW R1, lock

这个程序看起来没问题,但实际上有问题。以上代码存在之前提到的同步问题。让我们找到问题所在。假设锁的初始值为零。如果两个线程运行此代码,则其中一个可能在另一个读取lock变量之前到达SW R1,lock。因此,他们都认为lock是空闲的。 为解决此问题,提供了另一种指令而不是简单的LW和SW。它称为Read-Modify-Write指令。它是一个复杂的指令(由子指令组成),它确保锁获取过程仅由一个线程完成。与简单的Read和Write指令相比,Read-Modify-Write的区别在于它使用了不同的Loading和Storing方式。它使用LL(Load Linked)加载lock变量,并使用SC(Store Conditional)写入lock变量。额外的Link Register用于确保锁获取过程由单个线程完成。算法如下。
1. Define a variable called lock.
2. For each thread:
   2.1. Read the lock and put the address of lock variable inside the Link Register.
   2.2. If (lock == 0) and (&lock == Link Register), lock = 1 and reset the Link Register then goto 3    // Try to grab the lock
       Else goto 2.1    // Wait until the lock is released
3. Do something...
4. lock = 0    // Release the lock

当链接寄存器被重置时,如果另一个线程假设锁是自由的,则它将无法再次将递增值写入锁。因此,对变量的访问并发性被获取。 关键原子之间的核心区别在于以下想法:

为什么要使用锁(一个新变量),而不是使用实际变量(我们正在对其执行操作)作为锁变量?

使用新的变量作为会导致临界区,而使用实际变量作为锁将导致原子概念。当我们在实际变量上执行大量计算(多于一行)时,临界区是有用的。因为如果这些计算的结果无法写入实际变量,则整个过程应该重复计算结果。与等待锁被释放后再进入高度计算区域相比,这可能导致性能较差。因此,建议在想要执行单个计算(x ++,x--,++ x,- x等)时使用原子指令,并在通过密集部分进行更加计算复杂的区域时使用关键指令。

0

Critical子句将可变排除应用于代码块,并保证只有一个线程在给定时间执行代码块,线程完成代码块并输出另外的线程欢迎获得锁来执行该代码块。

Atomic子句仅适用于具有任何数学符号的单个语句,但差异不仅限于表达式的大小。原子子句保护赋值给左侧元素的地址位置,并仅保证对该变量的赋值。因此,您可以假设如果语句右侧存在任何函数调用,则可以并行执行它。

#pragma omp atomic
a = 5 + fnk();

这里fnk();可以被多个线程同时调用,但对a的赋值必须是互斥的。 如下所示,fnk()调用被另一个线程干扰,我们得到了结果0 2 2和0。如果我们使用了critical子句,情况就不会是这样。
enter image description here enter image description here


请忽略count是共享变量并且结果可能因迭代而异,重点关注在原子段发生的并发函数调用,但是分配操作是原子执行的。 - Osman

-6

atomic 是一个单语句的临界区,即你为了执行一个语句而进行锁定

临界区是对一段代码块进行锁定

一个好的编译器会像处理第一个代码一样处理第二个代码


1
那是错误的。请不要谈论你不了解的事情。 - jcsahnwaldt Reinstate Monica

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