我可以直接对原子变量进行算术运算吗?

Jas*_* Yu 12 c multithreading atomic language-lawyer stdatomic

我可以直接对原子变量进行算术运算吗?

因为我发现 C 标准库提供了很多实用函数,例如atomic_fetch_add执行原子变量和非原子变量之间的加法。但是,我很好奇,由于变量是原子的,我可以直接对其进行算术运算吗?就像下面所示的代码一样:

#include <threads.h>
#include <stdio.h>
#include <stdatomic.h>
atomic_int i = 0;
int run(void* v) {
  i += 100;  // <- is this operaiton thread-safe?
  // atomic_fetch_add(&i, 100);
  printf("%d\n", i);
  return thrd_success;
}
int main(void) {
  thrd_t thread;  
  thrd_create(&thread, run, NULL);
  thrd_join(thread, NULL);
  return 0; 
} 
Run Code Online (Sandbox Code Playgroud)

zwo*_*wol 11

C2011 中明确允许对具有原子类型的变量进行复合赋值:引用N1570 \xc2\xa76.15.6.2p3

\n
\n

形式的复合赋值E1 op= E2与简单赋值表达式等效E1 = E1 op (E2),只是左值E1仅计算一次,并且对于不确定顺序的函数调用,复合赋值的操作是单次计算。如果E1具有原子类型,则复合赋值是具有memory_order_seq_cst内存顺序语义的读取-修改-写入操作。[脚注113]

\n
\n

强调我的。脚注 113 继续给出了当 E1 是原子类型时E1 op= E2如何转换为原始操作的具体示例。<stdatomic.h>

\n

我确信可以应用于原子类型的其他运算符是 postfix++和--,它们也保证执行原子读取-修改-写入 (\xc2\xa76.5.2.4) 和简单赋值,这保证根据需要执行原子加载或存储 (\xc2\xa76.2.6.1)。

\n

注意: 前缀 ++和--不保证执行原子读-修改-写(比较 6.5.2.4 和 6.5.3.1)。

\n

我读了6.2.6.1这句话

\n
\n

具有原子类型的对象的加载和存储是通过memory_order_seq_cst语义完成的。

\n
\n

这意味着使用原子左值作为大多数其他运算符的操作数是有效的,并且执行原子加载,之后不会对该值进行特殊处理。不要在这部分引用我的话。

\n

C++ 可能有不同的规则。我将把 C++ 标准的解释留给其他人。

\n

  • [6.5.3.1](http://port70.net/~nsz/c/c11/n1570.html#6.5.3.1)说*表达式`++E`相当于`(E+=1)`*,没有关于原子的警告。因此,**前缀`++`/`--`实际上保证是原子RMW**,因为像`E += 1`这样的复合赋值操作是根据您在顶部引用的§6.15.6.2p3保证的您的答案。所以“++”毕竟不是 ISO C 中的陷阱。 (3认同)

Adr*_*ica 6

该i += 10;语句保持了 的原子性i,因为它被用作左值表达式。来自 cppreference(粗体我的):

内置递增和递减运算符以及复合赋值是读取-修改-写入原子操作,具有完全顺序一致的排序(就像使用 memory_order_seq_cst 一样)。如果需要不太严格的同步语义,则可以使用标准库函数。

链接页面上给出的示例使用变量的内置(预)增量操作acnt,但它也可以像您的代码一样使用复合赋值。

但是,如果不严格将其用作左值表达式,则更复杂的算术运算可能会导致i变量失去其原子性。来自同一页面:

原子属性仅对左值表达式有意义。左值到右值的转换(模拟从原子位置到 CPU 寄存器的内存读取)会剥离原子性以及其他限定符。