命名空间
变体
操作

memory_order

来自 cppreference.cn
< c‎ | atomic
在头文件 <stdatomic.h> 中定义
枚举 memory_order

{
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst

};
(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 使用此内存顺序的加载操作执行获取操作,存储操作执行释放操作,读-修改-写操作既执行获取操作又执行释放操作,此外还存在一个单一的总序,所有线程都以相同的顺序观察所有修改(参见下面的顺序一致排序)。

[编辑] 宽松排序

标记为 memory_order_relaxed 的原子操作不是同步操作;它们不强制并发内存访问之间的顺序。它们仅保证原子性和修改顺序一致性。

例如,在 xy 最初为零的情况下,

// 线程 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 可见。特别地,如果线程 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、PowerPC)上,使用特殊的 CPU 加载或内存屏障指令。

互斥锁,例如 互斥量原子自旋锁,是释放-获取同步的一个例子:当线程 A 释放锁并由线程 B 获取时,线程 A 上下文在临界区(释放之前)发生的一切都必须对执行相同临界区的线程 B(获取之后)可见。

[编辑] 顺序一致排序

标记为 memory_order_seq_cst 的原子操作不仅以与释放/获取排序相同的方式对内存进行排序(在一个线程中先于存储发生的一切都成为加载线程中的可见副作用),而且还建立了所有被如此标记的原子操作的单一总修改顺序

形式上,

每个从原子变量 M 加载的 memory_order_seq_cst 操作 B,观察以下之一:

  • 在单一总序中,M 的最后一次修改操作 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 观察以下之一:

  • 在单一总序中,M 的最后一次 memory_order_seq_cst 修改出现在 X 之前的结果;
  • M 的某些不相关的修改,这些修改在 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 的某些不相关的修改,这些修改在 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_fence X 和 Y,使得 A 先于 X,Y 先于 B,并且 X 在单一总序中出现在 Y 之前。

请注意,这意味着

1) 一旦未标记为 memory_order_seq_cst 的原子操作进入图片,顺序一致性就会丢失,
2) 顺序一致的屏障只为屏障本身建立总序,而不是在一般情况下为原子操作建立总序(先于不是跨线程关系,不像happens-before)。

对于多生产者-多消费者的情况,如果所有消费者都必须以相同的顺序观察所有生产者的操作,则可能需要顺序排序。

在所有多核系统上,总顺序排序需要一个完整的内存屏障 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 (p: 待定)
  • 7.17.3 顺序和一致性 (p: 待定)
  • C17 标准 (ISO/IEC 9899:2018)
  • 7.17.1/4 memory_order (p: 200)
  • 7.17.3 顺序和一致性 (p: 201-203)
  • C11 标准 (ISO/IEC 9899:2011)
  • 7.17.1/4 memory_order (p: 273)
  • 7.17.3 顺序和一致性 (p: 275-277)

[编辑] 另见

C++ 文档,关于 memory order

[编辑] 外部链接

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