并行编程中的READ_ONCE和WRITE_ONCE

aim*_*mar 6 c multithreading atomic volatile linux-kernel

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

\n
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))\n\n#define READ_ONCE(x) \\\n({ typeof(x) ___x = ACCESS_ONCE(x); ___x; })\n\n#define WRITE_ONCE(x, val) \\\ndo { ACCESS_ONCE(x) = (val); } while (0)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

__x宏结尾处的用法是什么READ_ONCE

\n

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

\n
\n

以下列出了允许对给定变量进行某些访问的简单加载和存储,而对同一变量的其他访问则需要标记(例如 READ_ONCE() 和 WRITE_ONCE())的情况:

\n
    \n
  1. 共享变量只能由给定的所属 CPU 或线程修改,但可以由其他 CPU 或线程读取。所有存储都必须使用 WRITE_ONCE()。所属的 CPU\nor 线程可以使用普通负载。其他一切都必须使用 READ_ONCE() 进行加载。
  2. \n
  3. 共享变量仅在持有给定锁时才会被修改,但会被不持有该锁的代码读取。所有存储都必须使用 WRITE_ONCE()。持有锁的 CPU 或线程可能会使用普通负载。其他一切都必须使用 READ_ONCE() 进行加载。
  4. \n
  5. 共享变量仅在给定拥有的 CPU 或线程持有给定锁时才会被修改,但会被其他 CPU 或线程或不持有该锁的代码读取。所有存储都必须使用 WRITE_ONCE()。拥有该锁的 CPU 或线程可以使用普通负载,任何持有锁的 CPU 或线程也可以使用普通负载。其他一切都必须使用\nREAD_ONCE() 进行加载。
  6. \n
  7. 共享变量只能由给定的 CPU 或线程以及在该 CPU\xe2\x80\x99s 或线程\xe2\x80\x99s 上下文中运行的信号或\n中断处理程序访问。处理程序可以使用普通加载和存储,任何阻止处理程序被调用的代码(即阻止信号和/或中断的代码)也可以使用。所有其他代码必须使用 READ_ONCE() 和 WRITE_ONCE()。
  8. \n
  9. 共享变量只能由给定的 CPU 或线程以及在该 CPU\xe2\x80\x99s 或线程\xe2\x80\x99s 上下文中运行的信号或\n中断处理程序访问,并且处理程序始终\n恢复任何值返回之前写入的变量。处理程序可以使用普通加载和存储,任何阻止处理程序被调用的代码(即阻止信号和/或中断的代码)也可以使用。所有其他代码都可以使用普通加载,但必须使用 WRITE_ONCE() 来防止存储撕裂、存储融合和发明存储。
  10. \n
\n
\n

首先我们如何使用这些宏来同时访问内存?AFAIK 易失性关键字对于并发内存访问并不安全。

\n

在第 1 项中,我们如何在没有数据竞争的情况下使用READ_ONCEWRITE_ONCE访问共享变量?

\n

在第 2 项中,WRITE_ONCE当只有持有锁才允许写入时,为什么他要使用宏。为什么read不需要持有锁?

\n

num*_*ero 9

这些宏是在支持编译器(GCC,也许其他一些)上强制执行某种程度的原子性(但不同步)的方法。它们在 Linux 中被大量使用,因为它远远早于 C11。

\n

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

\n

据我所知,这相当于使用带有 memory_order_relaxed 的 C++ 原子(如注释中指出的那样),但这些要求每次访问都是原子的,而在某些模式中实际上并不需要 \xe2\x80\x99s (例如,如果仅一个线程写入一个变量,它自己的读取 don\xe2\x80\x99t 需要是原子的,在这种情况下原子读写操作肯定是不必要的)。

\n