我能做到。
#pragma omp atomic
g_qCount++;
这不就跟
#pragma omp critical
g_qCount++;
?
#pragma omp atomic
g_qCount++;
这不就跟
#pragma omp critical
g_qCount++;
?
对于 g_qCount 的影响是相同的,但执行方式不同。
OpenMP 的临界区是完全通用的 - 它可以包围任意一段代码。然而,您需要为此付出代价,每次线程进入和退出临界区时都会产生显着的开销(除了串行化本身的成本之外)。
(此外,在 OpenMP 中,所有未命名的临界区都被认为是相同的(如果您愿意,所有未命名的临界区只有一个锁),因此如果一个线程在上面的一个 [未命名的] 临界区中,则没有线程能进入任何 [未命名的] 临界区。如您猜测的那样,您可以通过使用命名的临界区来解决这个问题)。
原子操作的开销要小得多。如果可用,它利用提供原子增量操作的硬件;在这种情况下,在输入/输出代码行时不需要锁定/解锁,它只执行硬件告诉你不能干扰的原子增量操作。
优点是开销更小,一个线程在原子操作中不会阻塞即将发生的任何(不同的)原子操作。缺点是原子支持的操作集受到限制。
当然,在任一情况下,您都需要承担串行化的成本。
在OpenMP中,所有未命名的关键段都是互斥的。
critical和atomic之间最重要的区别是,atomic只能保护单个赋值,并且您可以使用特定操作符。
临界区:
使用“name”标签可以将多个代码块串行化。
速度较慢!
原子操作:
速度更快!
仅保证特定操作的串行化。
最快的方法既不是关键的也不是原子的。大约来说,带关键段的加法比简单加法贵200倍,原子加法比简单加法贵25倍。
最快的选项(并非总是适用)是给每个线程分配一个计数器,并在需要总和时进行减少操作。
atomic
的局限性非常重要。它们应该在OpenMP规范中详细说明。由于Visual Studio 2012自2002年3月以来已经实现了OpenMP,因此MSDN提供了一个快速的备忘单,我不会感到惊讶如果这将不会改变。引用MSDN:
表达式语句必须采用以下形式之一:
x
binop=expr
x++
++x
x--
--x
在上述表达式中:
x
是具有标量类型的表达式。expr
是具有标量类型的表达式,并且不引用x
所指定的对象。binop不是重载运算符,而是+
、*
、-
、/
、&
、^
、|
、<<
或>>
之一。我建议在可以使用
atomic
的情况下使用它,在其他情况下使用命名关键部分。对其进行命名非常重要;这样您就可以避免调试方面的麻烦。
这里已经有很好的解释了。但是,我们可以深入一点。为了理解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
try: LW R1, lock
BNEZ R1, try
ADDI R1, R1, #1
SW R1, lock
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等)时使用原子指令,并在通过密集部分进行更加计算复杂的区域时使用关键指令。为什么要使用锁(一个新变量),而不是使用实际变量(我们正在对其执行操作)作为锁变量?
Critical
子句将可变排除应用于代码块,并保证只有一个线程在给定时间执行代码块,线程完成代码块并输出另外的线程欢迎获得锁来执行该代码块。
Atomic
子句仅适用于具有任何数学符号的单个语句,但差异不仅限于表达式的大小。原子子句保护赋值给左侧元素的地址位置,并仅保证对该变量的赋值。因此,您可以假设如果语句右侧存在任何函数调用,则可以并行执行它。
#pragma omp atomic
a = 5 + fnk();
这里fnk();
可以被多个线程同时调用,但对a的赋值必须是互斥的。
如下所示,fnk()调用被另一个线程干扰,我们得到了结果0 2 2和0。如果我们使用了critical子句,情况就不会是这样。
count
是共享变量并且结果可能因迭代而异,重点关注在原子段发生的并发函数调用,但是分配操作是原子执行的。 - Osmanatomic 是一个单语句的临界区,即你为了执行一个语句而进行锁定
临界区是对一段代码块进行锁定
一个好的编译器会像处理第一个代码一样处理第二个代码
++
和*=
这样的操作),并且如果硬件不支持它们,则可能会被“critical”部分替代。 - Dan R