memory_order
在标头 <stdatomic.h> 定义
|
||
enum memory_order { memory_order_relaxed, |
(C11 起) | |
memory_order
指定内存访问,包括常规的非原子内存访问,如何围绕原子操作排序。在没有任何制约的多处理器系统上,多个线程同时读或写数个变量时,一个线程能观测到变量值更改的顺序不同于另一个线程写它们的顺序。其实,更改的顺序甚至能在多个读取线程间相异。一些类似的效果还能在单处理器系统上出现,因为内存模型允许编译器变换。
语言和库中所有原子操作的默认行为提供序列一致顺序(见后述讨论)。该默认行为可能有损性能,不过可以给予库的原子操作额外的 memory_order
参数,以指定附加制约,在原子性外,编译器和处理器还必须强制该操作。
常量
在标头
<stdatomic.h> 定义 | |
值 | 解释 |
memory_order_relaxed
|
宽松操作:没有同步或顺序制约,仅对此操作要求原子性(见下方宽松顺序)。 |
memory_order_consume
|
有此内存顺序的加载操作,在其影响的内存位置进行消费操作:当前线程中依赖于当前加载的该值的读或写不能被重排到此加载前。其他释放同一原子变量的线程的对数据依赖变量的写入,为当前线程所可见。在大多数平台上,这只影响到编译器优化(见下方释放消费顺序)。 |
memory_order_acquire
|
有此内存顺序的加载操作,在其影响的内存位置进行获得操作:当前线程中读或写不能被重排到此加载前。其他释放同一原子变量的线程的所有写入,能为当前线程所见(见下方释放获得顺序)。 |
memory_order_release
|
有此内存顺序的存储操作进行释放操作:当前线程中的读或写不能被重排到此存储后。当前线程的所有写入,可见于获得该同一原子变量的其他线程(见下方释放获得顺序),并且对该原子变量的带依赖写入变得对于其他消费同一原子对象的线程可见(见下方释放消费顺序)。 |
memory_order_acq_rel
|
带此内存顺序的读修改写操作既是获得操作又是释放操作。当前线程的读或写内存不能被重排到此存储前或后。所有释放同一原子变量的线程的写入可见于修改之前,而且修改可见于其他获得同一原子变量的线程。 |
memory_order_seq_cst
|
有此内存顺序的加载操作进行获得操作,存储操作进行释放操作,而读修改写操作进行获得操作和释放操作,再加上存在一个单独全序,其中所有线程以同一顺序观测到所有修改(见下方序列一致顺序)。 |
本节未完成 原因:先发生于和其他概念同 C++ ,但保持修改顺序和c/language/atomic中的四种一致 |
本节未完成 原因:在做上面的时候,不要忘记在 C11 出版时先发生于不是不成环的,这经由 DR 401 被更新到匹配 C++11 |
宽松顺序
带标签 memory_order_relaxed 的原子操作不是同步操作;它们不会为并发的内存访问行为添加顺序约束。它们只保证原子性和修改顺序的一致性。
例如,对于初始值为零的x
和 y
,
// 线程 1 :
r1 = atomic_load_explicit(y, memory_order_relaxed); // A
atomic_store_explicit(x, r1, memory_order_relaxed); // B
// 线程 2 :
r2 = atomic_load_explicit(x, memory_order_relaxed); // C
atomic_store_explicit(y, 42, memory_order_relaxed); // D
结果可能会为 r1 == 42 && r2 == 42
。因为即使线程 1 中 A 先序于 B 且线程 2 中 C 先序于 D ,却没有约定去避免 D 中的对 y
的修改会在 A 之前 , B 中的对 x
的修改会在 C 之前 。 D 的副效应为它对 y
修改可能可见于 A 的加载操作, B 的副效应为它对 x
修改可能可见于 C 的加载操作。
宽松内存顺序的典型的应用是计数器自增,例如引用计数器,因为这只要求原子性,但不要求顺序或同步(注意计数器自减要求与析构函数进行获得释放同步)
释放消费顺序
若线程 A 中的原子存储带标签 memory_order_release 而线程 B 中来自同一对象的读取存储值的原子加载带标签 memory_order_consume ,则线程 A 视角中先发生于原子存储的所有内存写入(非原子和宽松原子的),会在线程 B 中该加载操作所携带依赖进入的操作中变成可见副效应,即一旦完成原子加载,则保证线程B中,使用从该加载获得的值的运算符和函数,能见到线程 A 写入内存的内容。
同步仅在释放和消费同一原子对象的线程间建立。其他线程能见到与被同步线程的一者或两者相异的内存访问顺序。
所有异于 DEC Alpha 的主流 CPU 上,依赖顺序是自动的,无需为此同步模式产生附加的 CPU 指令,只有某些编译器优化收益受影响(例如,编译器被禁止牵涉到依赖链的对象上的推测性加载)。
此顺序的典型使用情况,涉及对很少被写入的数据结构(安排表、配置、安全策略、防火墙规则等)的共时读取,和有指针中介发布的发布者-订阅者情形,即当生产者发布消费者能通过其访问信息的指针之时:无需令生产者写入内存的所有其他内容对消费者可见(这在弱顺序架构上可能是昂贵的操作)。这种场景的例子之一是 rcu 解引用。
注意到 2015 年 2 月为止没有产品编译器跟踪依赖链:均将消费操作提升为获得操作。
释放序列
若一些原子对象被存储-释放,而有数个其他线程对该原子对象进行读修改写操作,则会形成“释放序列”:所有对该原子对象读修改写的线程与首个线程同步,而且彼此同步,即使它们没有 memory_order_release
语义。这使得单产出-多消费情况可行,而无需在每个消费线程间强加不必要的同步。
释放获得顺序
若线程 A 中的一个原子存储带标签 memory_order_release ,而线程 B 中来自同一变量的原子加载带标签 memory_order_acquire ,则从线程 A 的视角先发生于原子存储的所有内存写入(非原子及宽松原子的),在线程 B 中成为可见副效应,即一旦原子加载完成,则保证线程 B 能观察到线程 A 写入内存的所有内容。
同步仅建立在释放和获得同一原子对象的线程之间。其他线程可能看到与被同步线程的一者或两者相异的内存访问顺序。
在强顺序系统( x86 、 SPARC TSO 、 IBM 主框架)上,释放获得顺序对于多数操作是自动进行的。无需为此同步模式添加额外的 CPU 指令,只有某些编译器优化受影响(例如,编译器被禁止将非原子存储移到原子存储-释放后,或将非原子加载移到原子加载-获得前)。在弱顺序系统( ARM 、 Itanium 、 Power PC )上,必须使用特别的 CPU 加载或内存栅栏指令。
互斥锁(例如互斥或原子自旋锁)是释放获得同步的例子:线程 A 释放锁而线程 B 获得它时,发生于线程 A 环境的临界区(释放之前)中的所有事件,必须对于执行同一临界区的线程 B (获得之后)可见。
序列一致顺序
带标签 memory_order_seq_cst 的原子操作不仅以与释放/获得顺序相同的方式排序内存(在一个线程中先发生于存储的任何结果都变成进行加载的线程中的可见副效应),还对所有带此标签的内存操作建立单独全序。
正式而言,
每个加载原子对象 M 的 memory_order_seq_cst
操作 B ,观测到以下之一:
- 修改 M 的上个操作 A 的结果,A 在单独全序中先出现于 B
- 或若存在这种 A ,则 B 可能观测到某些 M 的修改结果,这些修改非
memory_order_seq_cst
而且不先发生于 A - 或若不存在这种 A ,则 B 可能观测到某些 M 的无关联修改,这些修改非
memory_order_seq_cst
若存在 memory_order_seq_cst
的 atomic_thread_fence 操作 X 先序于 B ,则 B 观测到以下之一:
- 在单独全序中先出现于 X 的上个 M 的
memory_order_seq_cst
修改 - 在单独全序中后出现于它的某些 M 的无关联修改
设有一对 M 上的原子操作,称之为 A 和 B ,这里 A 写入、 B 读取 M 的值,若存在二个 memory_order_seq_cst
的 atomic_thread_fence X 和 Y ,且若 A 先序于 X , Y 先序于 B ,且 X 在单独全序中先出现于 Y ,则 B 观测到二者之一:
- A 的效应
- 某些在 M 的修改顺序中后出现于 A 的无关联修改
设有一对 M 上的原子操作,称之为 A 和 B ,若符合下列条件之一,则 M 的修改顺序中 B 先发生于 A
- 存在一个
memory_order_seq_cst
的 atomic_thread_fence X ,它满足 A 先序于 X ,且 X 在单独全序中先出现于 B - 或者,存在一个
memory_order_seq_cst
的 atomic_thread_fence Y ,它满足 Y 先序于 B ,且 A 在单独全序中先出现于 Y - 或者,存在
memory_order_seq_cst
的 atomic_thread_fence X 和 Y ,它们满足 A 先序于 X , Y 先序于 B ,且 X 在单独全序中先出现于 Y
注意这表明:
memory_order_seq_cst
标签的原子操作进入局面,则立即丧失序列一致性若在多生产者-多消费者的情形中,且所有消费者都必须以相同顺序观察到所有生产者的动作出现,则可能必须有序列顺序。
全序列顺序在所有多核系统上要求完全的内存栅栏 CPU 指令。这可能成为性能瓶颈,因为它强制受影响的内存访问传播到每个核心。
与 volatile 的关系
在执行线程中,不能将通过 volatile 左值的访问(读和写)重排到同线程内为序列点所分隔的可观测副效应(包含其他 volatile 访问)后,但不保证另一线程观察到此顺序,因为 volatile 访问不建立线程间同步。
另外, volatile 访问不是原子的(共时的读和写是数据竞争),且不排序内存(非 volatile 内存访问可以自由地重排到 volatile 访问前后)。
一个值得注意的例外是 Visual Studio ,其中默认设置下,每个 volatile 写拥有释放语义,而每个 volatile 读拥有获得语义( MSDN ),故而可将 volatile 对象用于线程间同步。标准的 volatile 语义不可应用于多线程编程,尽管它们在应用到 sig_atomic_t 对象时,足以与例如运行于同一线程的 signal 处理函数交流。
示例
本节未完成 原因:暂无示例 |
引用
- C17 标准(ISO/IEC 9899:2018):
- 7.17.1/4 memory_order (第 200 页)
- 7.17.3 Order and consistency (第 201-203 页)
- C11 标准(ISO/IEC 9899:2011):
- 7.17.1/4 memory_order (第 273 页)
- 7.17.3 Order and consistency (第 275-277 页)
参阅
外部链接
1. | MOESI 协议 |
2. | x86-TSO:x86 多处理器上严格而有用的程序员模型 P. Sewell 等,2010 |
3. | ARM 及 POWER 宽松内存模型的入门教程 P. Sewell 等,2012 |
4. | MESIF:点对点互联的两跳缓存一致性协议 J.R. Goodman, H.H.J. Hum,2009 |
5. | 内存模型 Russ Cox, 2021 |
本节未完成 原因:让我们在 QPI、MOESI,也许还有 Dragon 上找到好的参考资料。 |