C - 无锁共享内存访问中的易失性和内存障碍?

C - volatile and memory barriers in lockless shared memory access?

您好,我有一个关于在 C 中使用 volatile 和内存屏障 的一般性问题,同时在共享内存中进行内存更改,同时由多个线程在没有锁的情况下访问。 据我了解,易失性和内存屏障具有以下一般用途

  1. 内存障碍

A) 确保所有挂起的内存访问(read/writes(取决于屏障))在屏障之前已正确完成,然后才执行屏障之后的内存访问。

B) 确保编译器不会跨障碍重新排序 load/store 指令(取决于障碍)。

基本上,A 点的目的是处理乱序执行和写入缓冲区刷新延迟的情况,在这种情况下,处理器本身最终会对编译器生成的指令或上述指令进行的内存访问进行重新排序。 B 点的目的是当 C 代码被翻译成机器代码时,编译器本身不会在汇编中移动这些访问。

  1. 现在为 volatile volatile 基本上是以一种松散的方式表示的,这样编译器就不会在优化用 volatile 变量编写的代码时执行其优化。服务于以下广泛目的

A) 在将 C 代码转换为机器级代码时,内存访问不会缓存在 cpu 寄存器中,并且每次读入代码时,它都会转换为加载指令,通过汇编中的内存完成。

B) 当编译器将 C 代码转换为机器代码时,汇编中与其他 volatile 变量的内存访问的相对顺序保持相同的顺序,而汇编中的非 volatile 变量的内存访问可以交错。

我有以下问题

  1. 我的理解是否正确和完整?比如有没有我遗漏的情况或者我说的不正确。

  2. 因此,每当我们编写代码对多个线程并发访问的共享内存进行内存更改时,我们需要确保我们有屏障,以便对应点 1.A 和 1.B 不会发生。 对应于 2.B 的行为将由 1.B 处理,对于 2.A 我们需要将指针转换为易失性指针以进行访问。 基本上我试图理解我们是否应该始终将指针转换为易失性指针,然后进行内存访问,以便我们确定 2.A 不会发生,或者是否存在仅使用障碍就足够的情况?

  1. is my understanding correct and complete ?

是的,看起来是这样,除了没有提到 C11 <stdatomic.h> 几乎所有用途都已过时。

还有更多 bad/weird 没有 volatile(或更好,_Atomic)可能发生的事情您没有列出:LWN 文章 Who's afraid of a big bad optimizing compiler? 进入有关发明额外负载(并期望它们都读取相同的值)之类的事情的详细信息。它针对 Linux 内核代码,其中 C11 _Atomic 不是他们做事的方式。

除了 Linux 内核之外,新代码应该几乎总是使用 <stdatomic.h> 而不是使用 volatile 滚动你自己的原子和用于 RMW 和屏障的内联 asm。但是 确实 继续工作,因为我们 运行 线程跨越的所有 real-world CPU 都有一致的共享内存,所以在 asm 中进行内存访问就足够了inter-thread 可见度,如 memory_order_relaxed。请参阅 When to use volatile with multi threading?(基本上从不,除了在 Linux 内核中或可能已经很好地实现了 hand-rolled 东西的少数其他代码库)。

在 ISO C11 中,data-race 两个线程在同一个 object 上进行非同步读+写是未定义的行为,但主流编译器确实定义了行为,只是按照你想要的方式编译期待硬件保证或缺乏硬件保证发挥作用。


除此之外,是的,除了你的最后一个问题 2 外,看起来很准确:memory_order_relaxed 原子有 use-cases,就像 volatile 一样没有障碍,例如exit_now 标志。

or are there are cases where only using barriers suffice ?

不,除非你很幸运并且编译器碰巧生成了正确的 asm。

或者除非其他同步意味着此代码只有 运行 而没有其他线程是 reading/writing object。 (C++20 有 std::atomic_ref<T> 来处理代码的某些部分需要对数据进行原子访问,但程序的其他部分不需要的情况,而你想让它们 auto-vectorize或其他。C 还没有任何这样的东西,除了使用普通变量 with/without GNU C __atomic_load_n() 和其他内置函数,这就是 C++ headers 实现 std::atomic<T> 的方式, 这与 C11 _Atomic 编译到的底层支持相同。可能还有像 stdatomic.h 中定义的 atomic_load_explicit 这样的 C11 函数,但与 C++ 不同的是,_Atomic 是一个真正的关键字未在任何 header 中定义。)