为什么在使用原子时需要负载

Why are loads required when using atomics

在 Go(以及其他语言如 c++)中使用原子时,建议使用原子加载操作来读取并发写入的值。

如果原子写入(无论是存储还是整数增量)的定义(据我所知)是没有线程可以查看部分写入,那么为什么需要原子加载?

如果在该内存地址上只使用原子存储,内存地址的普通加载是否总是安全的?

这个答案主要针对 C 和 C++,因为我不直接熟悉许多其他语言的原子,但我怀疑它们是相似的。

在某些情况下,许多实际机器确实以这种方式工作。例如,在 x86-64 上,普通加载指令相对于普通存储或锁定的读-修改-写指令是原子的。所以对于可以用一条指令加载的类型,原则上可以使用普通赋值,避免撕裂。

但在某些情况下这是行不通的。例如:

  • 非无锁类型(例如,超过几个单词的结构)。在这种情况下,需要几条指令来加载或存储,因此必须在它们周围加锁,否则撕裂是完全可能的。 atomic 加载函数知道获取锁,普通赋值不会。

  • 可以无锁但需要特殊处理的类型。例如,x86-32 上的 64 位 long long int。普通加载将执行两个 32 位整数加载指令(它们各自是原子的),因此即使存储是原子的,它也可能发生在两者之间。但是 atomic 加载函数可以发出 64 位浮点或 SIMD 加载,效率较低但在一条原子指令中完成。 Example on godbolt.

因此,当存储和加载两者 使用提供的atomic 函数时,该语言承诺原子性。 - 你的“定义”对于 C 或 C++ 是不准确的。通过要求程序员始终使用 atomic 负载,该语言提供了一个“挂钩”,实现可以在需要时采取适当的行动。在普通负载就足够的情况下,实现可以相应地优化并且不会丢失任何内容。

另一点是 atomic 加载提供了一个地方,可以在需要时放置内存屏障(除 relaxed 之外的任何顺序)。一些架构包括带有内置屏障的加载指令(例如 ARM64 的 ldar),并且在语言级别将屏障作为加载的一部分可以使编译器更容易利用这一点。如果您必须在调用屏障函数后进行常规赋值,编译器将更难发现它可以将它们优化为 ldar.