memory_order
定义于头文件 <stdatomic.h> |
||
enum memory_order { |
(自 C11 起) | |
memory_order
指定了内存访问(包括常规的非原子内存访问)围绕原子操作的排序方式。在多核系统上,如果没有任何约束,当多个线程同时读取和写入多个变量时,一个线程可以观察到值的变化顺序与另一个线程写入的顺序不同。实际上,变化的明显顺序甚至可能在多个读取线程之间有所不同。由于内存模型允许的编译器转换,即使在单处理器系统上,也可能发生一些类似的效果。
在 语言 和库中,所有原子操作的默认行为都提供了顺序一致性排序(见下文讨论)。这种默认行为可能会损害性能,但库的原子操作可以接受额外的 memory_order
参数,以指定编译器和处理器必须为该操作强制执行的超出原子性的确切约束。
内容 |
[编辑] 常量
定义于头文件
<stdatomic.h> | |
值 | 解释 |
memory_order_relaxed
|
宽松操作:对其他读取或写入操作没有施加同步或排序约束,仅保证此操作的原子性(见下文宽松排序)。 |
memory_order_consume (在 C++26 中已弃用) |
具有此内存顺序的加载操作对受影响的内存位置执行消费操作:当前线程中依赖于当前加载值的任何读取或写入操作都不能在此加载之前重新排序。在其他线程中释放相同原子变量的数据依赖变量的写入在当前线程中可见。在大多数平台上,这仅影响编译器优化(见下文释放-消费排序)。 |
memory_order_acquire
|
具有此内存顺序的加载操作对受影响的内存位置执行获取操作:当前线程中的任何读取或写入操作都不能在此加载之前重新排序。其他线程中释放相同原子变量的所有写入在当前线程中可见(见下文释放-获取排序)。 |
memory_order_release
|
具有此内存顺序的存储操作执行释放操作:当前线程中的任何读取或写入操作都不能在此存储之后重新排序。当前线程中的所有写入在其他线程中获取相同原子变量时可见(见下文释放-获取排序),并且携带依赖关系到原子变量的写入在其他线程中消费相同原子变量时变得可见(见下文释放-消费排序)。 |
memory_order_acq_rel
|
具有此内存顺序的读取-修改-写入操作既是获取操作又是释放操作。当前线程中没有内存读取或写入操作可以在加载之前重新排序,也没有操作可以在存储之后重新排序。其他线程中释放相同原子变量的所有写入在修改之前可见,并且修改在其他线程中获取相同原子变量时可见。 |
memory_order_seq_cst
|
具有此内存顺序的加载操作执行获取操作,存储操作执行释放操作,读取-修改-写入操作既执行获取操作又执行释放操作,此外,还存在一个单一的总顺序,所有线程都以相同的顺序观察所有修改(见下文顺序一致性排序)。 |
本节尚不完整 原因:happens-before 和其他概念,如 C++ 中,但保留修改顺序和 c/language/atomic 中的四种一致性 |
本节尚不完整 原因:在进行上述操作时,不要忘记,尽管 happens-before 在已发布的 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 == r2 == 42,因为,尽管 A 在线程 1 中先于 B 排序,而 C 在线程 2 中先于 D 排序,但没有任何东西阻止 D 在 y 的修改顺序中出现在 A 之前,以及 B 在 x 的修改顺序中出现在 C 之前。D 对 y 的副作用可能对线程 1 中的加载 A 可见,而 B 对 x 的副作用可能对线程 2 中的加载 C 可见。特别是,如果 D 在线程 2 中在 C 之前完成(无论是由于编译器重新排序还是在运行时),则可能发生这种情况。
宽松内存排序的典型用途是递增计数器,例如引用计数器,因为这仅需要原子性,而不需要排序或同步。
[编辑] 释放-消费排序
如果线程 A 中的原子存储标记为 memory_order_release,线程 B 中来自同一变量的原子加载标记为 memory_order_consume,并且线程 B 中的加载读取了线程 A 中存储写入的值,则线程 A 中的存储依赖顺序先于线程 B 中的加载。
从线程 A 的角度来看,所有先于原子存储发生的内存写入(非原子和宽松原子)在线程 B 中成为可见的副作用,加载操作携带依赖关系到线程 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、PowerPC)上,使用特殊的 CPU 加载或内存栅栏指令。
互斥锁,例如 互斥量 或 原子自旋锁,是释放-获取同步的示例:当线程 A 释放锁,线程 B 获取锁时,在线程 A 的上下文中,临界区中发生的一切(在释放之前)必须对线程 B 可见(在获取之后),线程 B 正在执行相同的临界区。
[编辑] 顺序一致性排序
标记为 memory_order_seq_cst 的原子操作不仅以与释放/获取排序相同的方式对内存进行排序(在一个线程中的存储操作之前发生的一切都成为执行加载操作的线程中的可见副作用),而且还为所有标记为该顺序的原子操作建立一个单一的总修改顺序。
正式地,
每个从原子变量 M 加载的 memory_order_seq_cst
操作 B,观察到以下情况之一
- 在单一总顺序中出现在 B 之前的、修改 M 的最后一个操作 A 的结果,
- 或者,如果存在这样的 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 写入 M 的值,B 读取 M 的值,如果存在两个 memory_order_seq_cst
atomic_thread_fences X 和 Y,并且如果 A 先于 X 排序,Y 先于 B 排序,并且 X 在单一总顺序中出现在 Y 之前,则 B 观察到以下情况之一
- A 的效果,
- M 的一些不相关的修改,该修改在 M 的修改顺序中出现在 A 之后。
对于一对 M 的原子修改 A 和 B,如果满足以下条件,则 B 在 M 的修改顺序中发生在 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_fences X 和 Y,使得 A 先于 X 排序,Y 先于 B 排序,并且 X 在单一总顺序中出现在 Y 之前。
请注意,这意味着
memory_order_seq_cst
的原子操作,顺序一致性就会丢失,在多生产者-多消费者情况下,所有消费者必须以相同的顺序观察所有生产者的操作时,顺序排序可能是必要的。
总顺序排序需要在所有多核系统上使用完整的内存栅栏 CPU 指令。这可能会成为性能瓶颈,因为它强制受影响的内存访问传播到每个核心。
[编辑] 与 volatile 的关系
在执行线程中,通过 volatile 左值 的访问(读取和写入)不能在同一线程中被序列点分隔的可观察副作用(包括其他 volatile 访问)之后重新排序,但是另一个线程不保证观察到此顺序,因为 volatile 访问不建立线程间同步。
此外,volatile 访问不是原子的(并发读取和写入是数据竞争),并且不对内存进行排序(非 volatile 内存访问可以在 volatile 访问周围自由重新排序)。
一个值得注意的例外是 Visual Studio,在默认设置下,每个 volatile 写入都具有释放语义,每个 volatile 读取都具有获取语义(Microsoft Docs),因此 volatile 可以用于线程间同步。标准 volatile 语义不适用于多线程编程,尽管它们对于例如与在同一线程中运行的 信号 处理程序通信是足够的,当应用于 sig_atomic_t 变量时。
[编辑] 示例
本节尚不完整 原因:没有示例 |
[编辑] 参考文献
- C23 标准 (ISO/IEC 9899:2024)
- 7.17.1/4 memory_order (页码: 待定)
- 7.17.3 顺序和一致性 (页码: 待定)
- C17 标准 (ISO/IEC 9899:2018)
- 7.17.1/4 memory_order (页码: 200)
- 7.17.3 顺序和一致性 (页码: 201-203)
- C11 标准 (ISO/IEC 9899:2011)
- 7.17.1/4 memory_order (页码: 273)
- 7.17.3 顺序和一致性 (页码: 275-277)
[编辑] 参见
C++ 文档 关于 内存顺序
|
[编辑] 外部链接
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 的好参考文献。 |