GCC原子内置函数

28

我相信以下代码以原子方式增加了变量var的值。

http://gcc.gnu.org/onlinedocs/gcc-4.4.2/gcc/Atomic-Builtins.html

volatile int var = 0;
__sync_fetch_and_add( &var, 1 )

我理解上述代码的逻辑如下:

  1. 加载变量var的地址
  2. 以原子方式将数字1写入变量var - 通过寄存器/缓存等方式

然而,我怀疑以下操作是否也是原子操作

volatile int var = 0;
volatile int num = 1;
__sync_fetch_and_add( &var, num )

由于它可能被解释为

  1. 加载变量var的地址
  2. 将变量num的值加载到寄存器中
  3. 将该值写入变量var。

在执行#3之前,但在执行#2之后, CPU /线程被中断并且另一个CPU /线程更新了 变量num的值。

换句话说, 当使用gcc的_sync ()*()函数时, 我可以使用变量而不是常量作为第二个参数吗?

这不会破坏原子性吗?

1个回答

38

这个操作实际上是两个操作。

__sync_fetch_and_add( &var, num )

加载num是原子操作。将其添加到var也是原子操作。但是,当两个原子操作放在一起时,并不一定组成一个原子操作。这就是为什么发明新的无锁数据结构如此困难的原因。通常情况下,两个线程安全的操作组合在一起并不一定构成线程安全操作。这就是制作正确的多线程应用程序如此困难的原因。

你看,__sync_fetch_and_add虽然是原子的,但它的行为类似于普通函数--因此,它将"num"的当前值作为参数。说该函数的原子性被破坏并不完全正确--因为加载从num中读取的值是调用者的责任,而不是函数接口的一部分。我同样可以抱怨这一点:

__sync_fetch_and_add(&var, some_really_long_function());

或者更糟糕的是,

__sync_fetch_and_add(long_function_1(), long_function_2());

你说它“可能被解释为”

  1. 加载变量var的地址
  2. 加载变量num的值
  3. 执行原子加法

但是根据C规范,它不是“可能会”被这样解释,而是必须按照此方式解释,否则编译器将不符合规范(实际上,它可以交换#1和#2,但在这里这并不重要)。


我不会说加载num是原子的,因为这会超载原子工作的含义。在这里,我们使用它来表示整数加载没有字撕裂的意思,但也表示原子操作的意义,例如读写处理器屏障之间的缓存行锁定。 - user82238
@BlankXavier:但是这不是原子性的一般定义吗?换句话说,原子操作是指要么完全发生,要么完全不发生,这适用于许多常见体系结构上的所有字对齐的加载和存储。看起来你谈论的是有序保证(由内存屏障提供),这是原子性的不同概念——两个原子操作可能从不同进程中呈相反顺序发生,但这并不意味着它们不是原子的。 - Dietrich Epp
原子操作在内存屏障/缓存线锁定/LLSC等方面有着非常不同且更广泛的概念,而在读取普通整数并未出现字撕裂的情况下所谓的原子操作则相对简单。但大多数人并不知道内存屏障等问题,我认为使用相同的词来描述两种情况会造成混淆。 - user82238
1
我们不会将原子操作和内存屏障用同一个词来表示:原子操作是原子的,而内存屏障是内存屏障,它们是不同的概念,有不同的名称。然而,内存屏障可以被用来通过更基本的原子操作创建某些原子操作,并且你可以使用原子操作与内存屏障结合起来创建同步原语。大多数原子操作库都包括带有附加屏障的原子操作,因为它们在一起非常有用,但它们并不是同一件事情。 - Dietrich Epp
5
截至GCC 4.8版本,__sync内置函数已被弃用,推荐使用__atomic内置函数。这里只是为了方便任何想要使用__sync的人而留下的提示。 - lucidbrot
据我所知,num只是一个值,没有任何魔法参与,也没有原子性。 - Pavel Šimerda

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