并行编程中的 READ_ONCE 和 WRITE_ONCE

READ_ONCE and WRITE_ONCE in Parallel programming

在《Is Parallel Programming Hard, And, If So, You Can Do About It?》一书中,作者使用了几个我不明白它们实际作用的宏。

#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

#define READ_ONCE(x) \
({ typeof(x) ___x = ACCESS_ONCE(x); ___x; })

#define WRITE_ONCE(x, val) \
do { ACCESS_ONCE(x) = (val); } while (0)

我不明白 ACCESS_ONCE 宏的作用以及为什么它需要对 volatile 指针类型的对象进行强制转换和取消引用。

READ_ONCE宏末尾的__x有什么用?

下面还有一些我不理解的宏的用法。

Here is a list of situations allowing plain loads and stores for some accesses to a given variable, while requiring markings (such as READ_ONCE() and WRITE_ONCE()) for other accesses to that same variable:

  1. A shared variable is only modified by a given owning CPU or thread, but is read by other CPUs or threads. All stores must use WRITE_ONCE(). The owning CPU or thread may use plain loads. Everything else must use READ_ONCE() for loads.
  2. A shared variable is only modified while holding a given lock, but is read by code not holding that lock. All stores must use WRITE_ONCE(). CPUs or threads holding the lock may use plain loads. Everything else must use READ_ONCE() for loads.
  3. A shared variable is only modified while holding a given lock by a given owning CPU or thread, but is read by other CPUs or threads or by code not holding that lock. All stores must use WRITE_ONCE(). The owning CPU or thread may use plain loads, as may any CPU or thread holding the lock. Everything else must use READ_ONCE() for loads.
  4. A shared variable is only accessed by a given CPU or thread and by a signal or interrupt handler running in that CPU’s or thread’s context. The handler can use plain loads and stores, as can any code that has prevented the handler from being invoked, that is, code that has blocked signals and/or interrupts. All other code must use READ_ONCE() and WRITE_ONCE().
  5. A shared variable is only accessed by a given CPU or thread and by a signal or interrupt handler running in that CPU’s or thread’s context, and the handler always restores the values of any variables that it has written before return. The handler can use plain loads and stores, as can any code that has prevented the handler from being invoked, that is, code that has blocked signals and/or interrupts. All other code can use plain loads, but must use WRITE_ONCE() to prevent store tearing, store fusing, and invented stores.

首先我们如何使用这些宏同时访问内存?据我所知,volatile 关键字对于并发内存访问是不安全的。

在项目编号 1 中,我们如何使用 READ_ONCEWRITE_ONCE 访问共享变量而不发生数据竞争?

在第 2 项中,为什么他使用 WRITE_ONCE 宏,而只允许通过持有锁进行写入。为什么 read 不需要持有锁?

这些宏是在支持编译器(GCC,也许其他一些)上强制执行某种级别的原子性(但没有同步)的方法。它们在 Linux 中被大量使用,因为它比 C11 早了很多。

在 GCC 语义中,volatile 导致恰好发出一条访问指向值的指令(至少如果该值是字长的)。在 Linux 支持的所有体系结构中,对齐的字大小访问是原子的,因此整个构造导致一个单一的原子访问。 (我指的是machine word ofc,不是某些知名平台上的WORD类型)。

据我所知,这等同于使用带有 memory_order_relaxed 的 C++ 原子(正如评论中指出的那样),除了那些需要 every 访问是原子的虽然这在某些模式中实际上并不需要(例如,如果只有一个线程写入一个变量,它自己的读取不需要是原子的,并且原子读写操作 肯定 不必要在这种情况下)。