如果仅写入值,我是不是需要原子?

Posted

技术标签:

【中文标题】如果仅写入值,我是不是需要原子?【英文标题】:Do I need an atomic if a value is only written?如果仅写入值,我是否需要原子? 【发布时间】:2020-08-25 17:39:51 【问题描述】:

假设我有多个线程访问同一个内存位置。而且,如果有的话,它们都写入相同的值,而没有一个读取它。 之后,所有线程都会收敛(通过锁),然后我才读取该值。我需要为此使用原子吗? 这适用于 x86_64 系统。该值为 int32。

【问题讨论】:

你已经用汇编和 C 和 C++ 标记了这个。组装的答案肯定与其他两个不同。在汇编中,如果它是 4 字节对齐的,则对 dword 的每次写入都是原子的。 (而且通常即使它没有对齐。) 作为一般规则,如果您认为它需要是原子的,它可能会。 你关心谁的值出现在内存位置吗?最后写还是任何旧值? 在 C 和 C++ 中,您需要一个原子,因为最多允许一个并发编写器而无需同步。此外,在 C 和 C++ 中,编译器不需要在同步事件(as-if 规则)之前将 anything 写入内存。 @ThomasMatthews 确实如此,截至C11。 【参考方案1】:

根据§5.1.2.4 ¶25 and ¶4 of the ISO C11 standard,两个不同的线程使用非原子操作以无序方式写入同一内​​存位置会导致undefined behavior。如果所有线程都写入相同的值,ISO C 标准也不例外。

尽管 Intel/AMD 规范 x86/x64 CPU 保证将 32 位整数写入 4 字节对齐地址是原子的,但 ISO C 标准不保证这样的操作是原子的,除非您正在使用 ISO C 标准保证是原子的数据类型(例如 atomic_int_least32_t)。因此,即使您的线程将 int32_t 类型的值写入 4 字节对齐的地址,根据 ISO C 标准,您的程序仍然会导致未定义的行为。

但是,出于实际目的,假设编译器正在生成以原子方式执行操作的汇编指令可能是安全的,前提是满足对齐要求。

即使内存写入未对齐并且 CPU 不会原子地执行写入指令,您的程序很可能仍会按预期工作。一个写操作是否被分成两个写操作应该没有关系,因为所有线程都在写完全相同的值。

如果您决定不使用原子变量,那么您至少应该将该变量声明为volatile。否则,编译器可能会发出汇编指令,导致变量仅存储在 CPU 寄存器中,因此其他 CPU 可能永远看不到该变量的任何更改。

所以,回答您的问题:可能没有必要将您的变量声明为原子的。但是,仍然强烈推荐它。通常,对多个线程访问的变量的所有操作都应该是原子的或受mutex 保护。此规则的唯一例外是所有线程都对该变量执行只读操作。

玩弄未定义的行为可能很危险,通常不建议这样做。特别是,如果编译器检测到导致未定义行为的代码,则允许将该代码视为不可访问并对其进行优化。在某些情况下,一些编译器实际上会这样做。请参阅this very interesting post by Microsoft Blogger Raymond Chen 了解更多信息。

另外,请注意多个线程写入同一位置(甚至是相同的cache line)可能会破坏CPU pipeline,因为x86/x64 架构保证strong memory ordering 必须强制执行。如果 CPU 的cache coherency protocol 检测到由于另一个 CPU 写入同一高速缓存行而导致的内存顺序冲突,则可能必须清除整个 CPU 管道。因此,所有线程写入不同的内存位置(在不同的缓存行中,至少相隔 64 字节)并在所有线程同步后分析写入的数据可能会更有效。

【讨论】:

假设编译器正在生成以原子方式执行操作的汇编指令可能是安全的 - 是的,但假设存储或加载发生并不安全完全。除非您使用 volatile 滚动自己的原子,否则它们可以沉入或吊出循环 即使在具有弱序内存模型的 CPU 上,错误共享也是一个问题。它们仍然需要保持一致性,因此存储不能从存储缓冲区提交到 L1d 缓存,除非该行由该核心独占。 (在 MESI Exclusive 或 Modified 状态。)但是,是的,在 x86 上,错误共享也会对内存顺序错误推测管道核弹的读者产生额外的不良影响。这本身并不是一个stall;如果没有推测性的早期加载,x86 CPU 将不得不实际上按程序顺序执行加载,并在每次缓存未命中加载时停止。 @HadiBrais:您假设写入相同值的 2 个线程不会发生冲突。但是该标准确实在早期更严格地定义了术语“冲突”:同一部分的 p4:如果其中一个修改了内存位置,而另一个读取或修改了相同的内存位置,则两个表达式计算冲突。 写入相同的值也不例外;这在技术上是数据竞赛 UB。 @HadiBrais:我试图猜测您的确切论点是什么,感谢您的澄清。我对该措辞的理解是,两个分配/商店之间没有“发生在之前”的关系。即x = 42; 在两个单独的线程中,没有通过锁定或通过其他变量释放/获取同步的任何互斥。我看不出有任何方式写入相同的值会改变一个是否“发生在”另一个之前。 @HadiBrais:我不争辩它会在实际编译器上运行,并在读取前进行同步。只是它是 ISO C++ 中的 UB,因此 x86 的 DeathStation 9000 编译器可以做到 mov [x], 41 / inc [x] code-gen 如果它愿意(从 x 中发明读取),因为仅使用数据竞争 UB 破坏代码。是的,正如您所说,如果非同步部分编译为纯写入,则不需要原子性。 (但无论如何你都会有原子性,因为真正的编译器使用alignof(int)=4。)

以上是关于如果仅写入值,我是不是需要原子?的主要内容,如果未能解决你的问题,请参考以下文章

原子操作

如果读写是原子的,为啥需要 volatile [重复]

NSFileManager 多实例写入原子性

原子变量的对齐

为啥写入 24 位结构不是原子的(当写入 32 位结构时)?

用Python原子写入文件