+=、|=、&=等操作是否是原子操作?

33

"modify"运算符例如+=|=&=等是原子操作吗?

我知道++是原子操作(如果你在两个不同的线程中同时执行x++;,你最终得到的结果是x增加了2,与关闭优化的x=x+1不同)。

我的疑问是variable |= constant这样的操作是否是线程安全的,或者我必须用互斥锁来保护它们?

(...还是这取决于CPU?如果是这种情况,在ARM上会怎样?)


1
哪个ARM版本?如果编译器支持或者你自己编写汇编代码,v6(ARM10)及更高版本的架构可以提供原子操作。早期的架构则不行。 - Mike Seymour
gcc具有内置的原子操作:http://gcc.gnu.org/onlinedocs/gcc-4.4.3/gcc/Atomic-Builtins.html#Atomic-Builtins;请注意:“并非所有操作都被所有目标处理器支持”。 - Christoph
这里有用于交替变量访问的Windows API:http://msdn.microsoft.com/en-us/library/ms684122%28v=VS.85%29.aspx - Janusz Lenar
1
除了语言本身不保证之外,x86-64具有强大的内存模式(加载获取和存储释放)。即使在这种情况下,例如INC m64作为RMW操作也不能保证是原子性的,除非使用LOCK前缀。 - Arne Vogel
12个回答

76

你错了。没有任何保证++是原子的,对于复合赋值运算符也是如此,事实上对于任何C++操作都是如此。


4
这意味着这是与CPU相关的。在允许inc [address]的单核架构中,这绝对是原子操作。 - SF.
2
至少直到C++0x之前:“本条款描述了用于细粒度原子访问的组件。这种访问是通过对原子对象进行操作来实现的。” [n3035草案中的29.1/1] - Roger Pate
27
不是这样的。这是与编译器相关的。仅仅因为 CPU 架构有一条指令,并不意味着编译器会以你期望的方式使用它,甚至可能根本不使用。 - anon
3
即使编译器可以生成原子增量,x++是否是原子的可能取决于x的数据类型。例如,在SF的目标上,如果x是“long long”,对x的递增将不是原子操作;如果它是另一种整数类型,则可能取决于特定的ARM架构和数据对齐方式。 - Clifford
2
@SF:非常具体。如果编译器选择不使用该指令怎么办?如果/当您升级到多核系统时呢?那些没有 inc 指令的架构怎么办?总的来说++ 不是 原子操作。您只是找到了一个单一狭窄的特例,在这种情况下它并不是一个问题。 - jalf
@SF:我甚至认为在x86上inc [address]不是原子操作。 - user541686

9

x++经常用3个指令实现:将X读入寄存器,增加它,然后将其写回内存。

在这些指令之间,您的线程可能会被抢占。


2
在某些处理器上,即使是单个指令也可能是非原子性的。 - Jive Dadson

6
为了让值的变化在多个核心中可见,例如 a += 的操作需要加载值、加上增量并存储。这意味着该操作不是原子性的
为确保原子性,需要在操作周围放置适当的锁定。

3

C/C++中没有任何运算符是保证原子性的。它们在你的平台上可能是原子的,但你不会确定。通常,唯一原子操作是一个测试和设置指令,这通常作为实现信号量的基础,在大多数现代CPU中以某种形式可用。


3

2

这是编译器和CPU相关的。一些指令集提供了这些操作的原子指令(用于机器大小的整数)。

但是,并不保证你的编译器会使用这些指令,并且不会以非线程安全的方式优化代码。你需要编写汇编程序或使用特定于编译器的技术(例如intrinsics),以提供原子性(或使用使用这些技术之一的库)。


在ARM架构中: ORR/ADD/AND指令需要两个操作数并将结果放置在寄存器中。任意一个操作数都可以是与结果寄存器相同的寄存器,因此它们可以用作原子 |=、+=、&= 操作。

当然,结果会被放置在寄存器中,而第一个操作数也必须来自寄存器,因此你必须确保寄存器加载是原子的。


2

在您的编译器/平台上,++可能是原子性的,但在C++规范中并未定义为原子性。

如果您想确保以原子方式修改值,则应使用适当的方法,例如Windows上的Interlocked*。

对于所有其他例程也是如此。如果要进行原子操作,则应使用适当的调用而不是标准调用。


1
一个重复的指向了这里,需要进行更新。"新的" C11 语言引入了一种原子属性,允许:
_Atomic int a;
...
a += 3

可以编译成一个(原子)无限循环。感谢标准制定者的礼物,但我真的希望你没有这样做。

1:在某些架构中,仅支持特定访问协议的内存才能进行原子操作。例如,ARMv7、MIPS将该序列转换为:

do {
    x = LoadLinked(a) + 3;
} while !StoreConditional(x, &a);

但对于一些内存/缓存类型,LoadLinked/StoreConditional未定义。享受调试吧。

2: 相关的是虚假共享,这是LoadLinked、StoreConditional在缓存行(例如32、64、256字节)而不是子块上操作的产物。因此: _Atomic int a[4]; 可能需要4倍的缓存行大小(因此是1024字节)才能安全地允许同时对a[n]和a[n+1]进行原子操作,因为4个CPU可能会争夺更新a[0..3],但从未成功。

希望下一个标准能认识到属性修饰的固有失败,并恢复c89作为合适的C标准。


1

0

你必须保护你的变量,例如使用互斥锁


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