memory_order
在标头 <stdatomic.h> 定义
|
||
enum memory_order { |
(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,却无法避免在 y 的修改顺序中 D 会出现于 A 之前,且在 x 的修改顺序中 B 会出现于 C 之前。D 的对 y 的副效应可能可见于线程 1 中 A 的加载操作,而 B 对 x 的副效应可能可见于线程 2 中 C 的加载操作。尤其是,这可能在线程 2 中 D 于 C 之前完成的情况下发生,无论因为编译器重排还是发生于运行时。
宽松内存定序的典型的应用是计数器自增,例如引用计数器,因为这只要求原子性,但不要求定序或同步。
释放消费顺序
若线程 A 中的原子存储被标以 memory_order_release,而线程 B 中从同一变量的原子加载被标以 memory_order_consume,而线程 B 中的加载读到了由线程 A 中的存储所写入的值,则线程 A 中的存储按依赖先序于线程 B 中的加载。
线程 A 视角中先发生于原子存储的所有内存写入(非原子和宽松原子的),会在线程 B 中该加载操作所携带依赖进入的操作中变成可见副效应,即一旦完成原子加载,则保证线程 B 中,使用从该加载获得的值的运算符和函数,能见到线程 A 写入内存的内容。
同步仅在释放和消费同一原子变量的线程间建立。其他线程能见到与被同步线程的一者或两者相异的内存访问顺序。
在除 DEC Alpha 之外的所有主流 CPU 上,依赖定序是自动的,无需为此同步模式发出额外的 CPU 指令,只有某些编译器优化会受影响(例如,编译器被禁止牵涉到依赖链的对象上的推测性加载)。
此定序的典型使用情况,包括对很少被写入的并发数据结构(路由表、配置、安全策略、防火墙规则等)的读取访问,和有指针中介发布的发布者-订阅者的情形,即生产者所发布的指针,消费者能通过其访问信息:无需令生产者写入内存的所有其他内容对消费者可见(这在弱顺序架构上可能是昂贵的操作)。这种场景的例子之一是 rcu_dereference
。
注意到 2015 年 2 月为止没有任何已知产品级编译器跟踪依赖链:消费操作均被提升为获得操作。
释放序列
若一些原子对象被存储-释放,而有数个其他线程对该原子对象进行读修改写操作,则会形成“释放序列”:所有对该原子对象读修改写的线程与首个线程同步,而且彼此同步,即使它们没有 memory_order_release
语义。这使得单产出-多消费情况可行,而无需在每个消费线程间强加不必要的同步。
释放获得顺序
若线程 A 中的一个原子存储被标以 memory_order_release,而线程 B 中从同一变量的原子加载被标以 memory_order_acquire,且线程 B 中的加载读到了线程 A 中的存储所写入的值,则线程 A 中的存储同步于线程 B 中的加载。
从线程 A 的视角先发生于原子存储的所有内存写入(包括非原子及宽松原子的),在线程 B 中成为可见副效应。即一旦原子加载完成,则保证线程 B 能观察到线程 A 写入内存的所有内容。仅当 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 读拥有获得语义(微软文档),故而可将 volatile 对象用于线程间同步。标准的 volatile 语义不可应用于多线程编程,尽管它们在应用到 sig_atomic_t 对象时,足以用于例如与运行于同一线程的 signal 处理函数间的通信。
示例
本节未完成 原因:暂无示例 |
引用
- C23 标准(ISO/IEC 9899:2024):
- 7.17.1/4 memory_order (第 TBD 页)
- 7.17.3 Order and consistency (第 TBD 页)
- 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 上找到好的参考资料。 |