std::memory_order
定义于头文件 <atomic> |
||
enum memory_order { |
(C++11 起) (C++20 前) |
|
enum class memory_order : /* 未指定 */ { |
(C++20 起) | |
std::memory_order
指定了内存访问,包括常规的非原子内存访问,如何在原子操作周围进行排序。在一个多核系统上,如果没有约束,当多个线程同时读写多个变量时,一个线程观察到的值变化的顺序可能与另一个线程写入的顺序不同。事实上,变化的表观顺序甚至在多个读取线程之间也可能不同。即使在单处理器系统上,由于内存模型允许的编译器转换,也可能发生一些类似的效果。
库中所有原子操作的默认行为提供*顺序一致性排序*(参见下面的讨论)。该默认行为可能会影响性能,但库的原子操作可以给定一个额外的 std::memory_order
参数来指定编译器和处理器必须为该操作强制执行的精确约束,除了原子性之外。
目录 |
[编辑] 常量
定义于头文件
<atomic> | |
名称 | 解释 |
memory_order_relaxed
|
宽松操作:不施加任何同步或排序约束于其他读写,仅保证此操作的原子性(参见下方宽松排序)。 |
memory_order_consume (C++26 中已弃用) |
此内存顺序的加载操作在受影响的内存位置执行*消费操作*:当前线程中依赖于当前加载的值的任何读或写不能在此加载之前重新排序。在其他线程中,释放相同原子变量的数据依赖变量的写入在当前线程中可见。在大多数平台上,这仅影响编译器优化(参见下方释放-消费排序)。 |
memory_order_acquire
|
此内存顺序的加载操作在受影响的内存位置执行*获取操作*:当前线程中的任何读或写不能在此加载之前重新排序。在其他线程中,释放相同原子变量的所有写入在当前线程中可见(参见下方释放-获取排序)。 |
memory_order_release
|
此内存顺序的存储操作执行*释放操作*:当前线程中的任何读或写不能在此存储之后重新排序。当前线程中的所有写入在获取相同原子变量的其他线程中可见(参见下方释放-获取排序),并且携带依赖到原子变量的写入在消费相同原子的其他线程中可见(参见下方释放-消费排序)。 |
memory_order_acq_rel
|
此内存顺序的读-修改-写操作既是*获取操作*又是*释放操作*。当前线程中的任何内存读或写不能在此加载之前或此存储之后重新排序。在其他线程中,释放相同原子变量的所有写入在修改之前可见,并且该修改在获取相同原子变量的其他线程中可见。 |
memory_order_seq_cst
|
此内存顺序的加载操作执行*获取操作*,存储操作执行*释放操作*,读-修改-写操作既执行*获取操作*又执行*释放操作*,此外还存在一个单一的总序,所有线程都以相同的顺序观察所有修改(参见下方顺序一致性排序)。 |
[编辑] 正式描述
线程间同步和内存排序决定了表达式的*求值*和*副作用*如何在不同的执行线程之间排序。它们通过以下术语定义:
[编辑] 顺序前 (Sequenced-before)
在同一线程内,求值 A 可能*顺序前*求值 B,如求值顺序中所述。
携带依赖 (Carries dependency)在同一线程内,如果以下任一条件为真,则*顺序前*求值 B 的求值 A 也可能*携带依赖*到 B(即,B 依赖于 A): 1) A 的值用作 B 的操作数,**除了**
a) 如果 B 是对 std::kill_dependency 的调用,
b) 如果 A 是内置 &&、||、?: 或 , 运算符的左操作数。
2) A 写入标量对象 M,B 从 M 读取。
3) A 携带依赖到另一个求值 X,且 X 携带依赖到 B。
|
(直到 C++26) |
[编辑] 修改顺序
对任何特定原子变量的所有修改都按照一个特定于该原子变量的总顺序发生。
所有原子操作都保证以下四个要求:
[编辑] 释放序列
在对原子对象 M 执行*释放操作* A 之后,M 的修改顺序中由以下两部分组成的最长连续子序列:
1) 由执行 A 的同一线程执行的写入。
|
(C++20 前) |
被称为*以 A 为头的释放序列*。
[编辑] 同步于 (Synchronizes with)
如果线程 A 中的原子存储是*释放操作*,线程 B 从同一变量的原子加载是*获取操作*,并且线程 B 中的加载读取了线程 A 中存储的值,那么线程 A 中的存储*同步于*线程 B 中的加载。
此外,一些库调用可能被定义为*同步于*其他线程上的其他库调用。
依赖有序前 (Dependency-ordered before)在线程之间,如果以下任一条件为真,则求值 A *依赖有序前*求值 B: 1) A 在某个原子 M 上执行*释放操作*,并且在不同的线程中,B 在相同的原子 M 上执行*消费操作*,且 B 读取了由以 A 为头的释放序列的任何部分写入的(直到 C++20)值。
2) A *依赖有序前* X,且 X *携带依赖*到 B。
|
(直到 C++26) |
[编辑] 线程间先行发生 (Inter-thread happens-before)
在线程之间,如果以下任一条件为真,则求值 A *线程间先行发生*求值 B:
先行发生 (Happens-before)无论线程如何,如果以下任一条件为真,则求值 A *先行发生*求值 B: 1) A *顺序前* B。
2) A *线程间先行发生* B。
实现要求通过引入额外的同步(仅当涉及消费操作时才可能需要,参见 Batty 等人)来确保*先行发生*关系是无环的。 如果一个求值修改了一个内存位置,而另一个读取或修改了相同的内存位置,并且如果至少一个求值不是原子操作,则程序的行为是未定义的(程序存在数据竞争),除非在这些两个求值之间存在*先行发生*关系。
|
(直到 C++26) | ||
先行发生 (Happens-before)无论线程如何,如果以下任一条件为真,则求值 A *先行发生*求值 B: 1) A *顺序前* B。
2) A *同步于* B。
3) A *先行发生* X,且 X *先行发生* B。 |
(C++26 起) |
[编辑] 强先行发生 (Strongly happens-before)
无论线程如何,如果以下任一条件为真,则求值 A *强先行发生*求值 B:
1) A *顺序前* B。
2) A *同步于* B。
3) A *强先行发生* X,且 X *强先行发生* B。 |
(C++20 前) | ||
1) A *顺序前* B。
2) A *同步于* B,且 A 和 B 都是顺序一致的原子操作。
3) A *顺序前* X,X 简单(直到 C++26) *先行发生* Y,且 Y *顺序前* B。
4) A *强先行发生* X,且 X *强先行发生* B。
注意:非正式地,如果 A *强先行发生* B,那么在所有上下文中 A 都显得在 B 之前求值。
|
(C++20 起) |
[编辑] 可见副作用
对标量 M 的副作用 A(写入)对于 M 的值计算 B(读取)是*可见的*,如果以下两个条件都为真:
如果副作用 A 对于值计算 B 是可见的,那么在*修改顺序*中,对 M 的副作用的最长连续子集(其中 B 不*先行发生*它)被称为*可见副作用序列*(M 的值,由 B 确定,将是这些副作用之一存储的值)。
注意:线程间同步归结为防止数据竞争(通过建立先行发生关系)和定义在什么条件下什么副作用变得可见。
[编辑] 消费操作
使用 memory_order_consume
或更强内存顺序的原子加载是消费操作。请注意,std::atomic_thread_fence 施加了比消费操作更强的同步要求。
[编辑] 获取操作
使用 memory_order_acquire
或更强内存顺序的原子加载是获取操作。Mutex 上的 lock()
操作也是获取操作。请注意,std::atomic_thread_fence 施加了比获取操作更强的同步要求。
[编辑] 释放操作
使用 memory_order_release
或更强内存顺序的原子存储是释放操作。Mutex 上的 unlock()
操作也是释放操作。请注意,std::atomic_thread_fence 施加了比释放操作更强的同步要求。
[编辑] 解释
[编辑] 宽松排序
标记为 memory_order_relaxed 的原子操作不是同步操作;它们不会在并发内存访问之间施加顺序。它们仅保证原子性和修改顺序一致性。
例如,如果 x 和 y 最初为零,
// Thread 1: r1 = y.load(std::memory_order_relaxed); // A x.store(r1, std::memory_order_relaxed); // B // Thread 2: r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::memory_order_relaxed); // D
允许产生 r1 == r2 == 42,因为尽管线程 1 中的 A *顺序前* B,且线程 2 中的 C *顺序前* D,但没有什么能阻止 D 在 y 的修改顺序中出现在 A 之前,以及 B 在 x 的修改顺序中出现在 C 之前。D 对 y 的副作用可能对线程 1 中的加载 A 可见,而 B 对 x 的副作用可能对线程 2 中的加载 C 可见。特别地,这可能发生在线程 2 中 D 在 C 之前完成的情况下,无论是由于编译器重排还是运行时。
即使在宽松的内存模型下,也不允许“凭空产生”的值循环依赖于其自身的计算,例如,如果 x 和 y 最初为零, // Thread 1: r1 = y.load(std::memory_order_relaxed); if (r1 == 42) x.store(r1, std::memory_order_relaxed); // Thread 2: r2 = x.load(std::memory_order_relaxed); if (r2 == 42) y.store(42, std::memory_order_relaxed); 不允许产生 r1 == r2 == 42,因为只有当对 x 的存储存储 42 时,对 y 的存储 42 才可能,这又循环依赖于对 y 的存储存储 42。注意,直到 C++14,这在技术上是规范允许的,但不建议实现者这样做。 |
(C++14 起) |
宽松内存排序的典型用途是递增计数器,例如 std::shared_ptr 的引用计数器,因为这只需要原子性,而不需要排序或同步(请注意,递减 std::shared_ptr
计数器需要与析构函数进行获取-释放同步)。
#include <atomic> #include <iostream> #include <thread> #include <vector> std::atomic<int> cnt = {0}; void f() { for (int n = 0; n < 1000; ++n) cnt.fetch_add(1, std::memory_order_relaxed); } int main() { std::vector<std::thread> v; for (int n = 0; n < 10; ++n) v.emplace_back(f); for (auto& t : v) t.join(); std::cout << "Final counter value is " << cnt << '\n'; }
输出
Final counter value is 10000
[编辑] 释放-获取排序
如果线程 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 加载或内存屏障指令。
互斥锁,例如 std::mutex 或 原子自旋锁,是释放-获取同步的一个例子:当线程 A 释放锁并由线程 B 获取时,线程 A 上下文中的临界区(在释放之前)发生的一切都必须对执行相同临界区的线程 B(在获取之后)可见。
#include <atomic> #include <cassert> #include <string> #include <thread> std::atomic<std::string*> ptr; int data; void producer() { std::string* p = new std::string("Hello"); data = 42; ptr.store(p, std::memory_order_release); } void consumer() { std::string* p2; while (!(p2 = ptr.load(std::memory_order_acquire))) ; assert(*p2 == "Hello"); // never fires assert(data == 42); // never fires } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); }
以下示例演示了使用释放序列在三个线程之间进行传递性释放-获取排序。
#include <atomic> #include <cassert> #include <thread> #include <vector> std::vector<int> data; std::atomic<int> flag = {0}; void thread_1() { data.push_back(42); flag.store(1, std::memory_order_release); } void thread_2() { int expected = 1; // memory_order_relaxed is okay because this is an RMW, // and RMWs (with any ordering) following a release form a release sequence while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) { expected = 1; } } void thread_3() { while (flag.load(std::memory_order_acquire) < 2) ; // if we read the value 2 from the atomic flag, we see 42 in the vector assert(data.at(0) == 42); // will never fire } int main() { std::thread a(thread_1); std::thread b(thread_2); std::thread c(thread_3); a.join(); b.join(); c.join(); }
[编辑] 释放-消费排序
如果线程 A 中的原子存储标记为 memory_order_release,线程 B 从同一变量的原子加载标记为 memory_order_consume,并且线程 B 中的加载读取了线程 A 中存储的值,那么线程 A 中的存储*依赖有序前*线程 B 中的加载。 从线程 A 的角度来看,所有*先行发生*原子存储的内存写入(非原子和宽松原子)都成为线程 B 中那些加载操作*携带依赖*的操作中的*可见副作用*,也就是说,一旦原子加载完成,线程 B 中使用从加载中获取的值的运算符和函数就保证能看到线程 A 写入内存的内容。 同步仅在*释放*和*消费*相同原子变量的线程之间建立。其他线程可能看到与一个或两个同步线程不同的内存访问顺序。 在除 DEC Alpha 之外的所有主流 CPU 上,依赖排序是自动的,无需为此同步模式发出额外的 CPU 指令,仅某些编译器优化受到影响(例如,编译器被禁止对涉及依赖链的对象执行推测性加载)。 此排序的典型用例涉及对很少写入的并发数据结构(路由表、配置、安全策略、防火墙规则等)的读取访问,以及使用指针介导的发布(即当生产者发布一个指针,消费者可以通过该指针访问信息)的发布-订阅场景:无需使生产者写入内存的所有其他内容对消费者可见(这在弱有序架构上可能是一个昂贵的操作)。这种场景的一个例子是 另请参阅 std::kill_dependency 和 请注意,目前(2015 年 2 月)没有已知的生产编译器跟踪依赖链:消费操作被提升为获取操作。 |
(直到 C++26) |
释放-消费排序的规范正在修订中,暂时不鼓励使用 |
(C++17 起) (直到 C++26) |
释放-消费排序与释放-获取排序具有相同的效果,并且已被弃用。 |
(C++26 起) |
此示例演示了指针介导发布的依赖有序同步:整数数据通过数据依赖关系与字符串指针不相关,因此其值在消费者中是未定义的。
#include <atomic> #include <cassert> #include <string> #include <thread> std::atomic<std::string*> ptr; int data; void producer() { std::string* p = new std::string("Hello"); data = 42; ptr.store(p, std::memory_order_release); } void consumer() { std::string* p2; while (!(p2 = ptr.load(std::memory_order_consume))) ; assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr assert(data == 42); // may or may not fire: data does not carry dependency from ptr } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); }
[编辑] 顺序一致性排序
标记为 memory_order_seq_cst 的原子操作不仅以与释放/获取排序相同的方式对内存进行排序(在一个线程中*先行发生*存储的所有内容在执行加载的线程中成为*可见副作用*),而且还建立了所有如此标记的原子操作的*单一总修改顺序*。
形式上, 每个从原子变量 M 加载的
如果存在一个
对于 M 上的原子操作 A 和 B,其中 A 写入且 B 读取 M 的值,如果存在两个
对于 M 上的原子修改 A 和 B,如果满足以下任一条件,则 B 在 M 的修改顺序中出现在 A 之后:
请注意,这意味着 1) 一旦未标记
memory_order_seq_cst 的原子操作进入图片,程序的顺序一致性就丧失了,2) 在许多情况下, memory_order_seq_cst 原子操作可以与同一线程执行的其他原子操作重新排序(*顺序前*不是跨线程关系,不像*先行发生*)。 |
(C++20 前) |
形式上, 原子操作 A 在某个原子对象 M 上*一致性有序前*另一个原子操作 B 在 M 上,如果以下任一条件为真: 1) A 是一个修改,并且 B 读取 A 存储的值,
2) A 在 M 的*修改顺序*中先于 B,
3) A 读取原子修改 X 存储的值,X 在*修改顺序*中先于 B,并且 A 和 B 不是同一个原子读-修改-写操作,
4) A *一致性有序前* X,且 X *一致性有序前* B。
所有 1) 如果 A 和 B 是
memory_order_seq_cst 操作,且 A *强先行发生* B,则 A 在 S 中先于 B,2) 对于对象 M 上的每对原子操作 A 和 B,其中 A *一致性有序前* B
a) 如果 A 和 B 都是
memory_order_seq_cst 操作,则 A 在 S 中先于 B,b) 如果 A 是
memory_order_seq_cst 操作,且 B *先行发生* memory_order_seq_cst 屏障 Y,则 A 在 S 中先于 Y,c) 如果
memory_order_seq_cst 屏障 X *先行发生* A,且 B 是 memory_order_seq_cst 操作,则 X 在 S 中先于 B,d) 如果
memory_order_seq_cst 屏障 X *先行发生* A,且 B *先行发生* memory_order_seq_cst 屏障 Y,则 X 在 S 中先于 Y。正式定义确保 1) 单一总序与任何原子对象的*修改顺序*一致,
2)
memory_order_seq_cst 加载要么从最后一次 memory_order_seq_cst 修改获取其值,要么从不*先行发生*先行 memory_order_seq_cst 修改的非 memory_order_seq_cst 修改获取其值。单一总序可能与*先行发生*不一致。这允许在某些 CPU 上更有效地实现 例如,如果 `x` 和 `y` 最初为零, // Thread 1: x.store(1, std::memory_order_seq_cst); // A y.store(1, std::memory_order_release); // B // Thread 2: r1 = y.fetch_add(1, std::memory_order_seq_cst); // C r2 = y.load(std::memory_order_relaxed); // D // Thread 3: y.store(3, std::memory_order_seq_cst); // E r3 = x.load(std::memory_order_seq_cst); // F 允许产生 r1 == 1 && r2 == 3 && r3 == 0,其中 A *先行发生* C,但 C 在 注意: 1) 一旦未标记
memory_order_seq_cst 的原子操作进入,程序的顺序一致性保证就丢失了,2) 在许多情况下, memory_order_seq_cst 原子操作可以与同一线程执行的其他原子操作重新排序。 |
(C++20 起) |
对于多生产者-多消费者场景,其中所有消费者必须以相同的顺序观察所有生产者的操作,顺序排序可能是必要的。
总顺序一致性在所有多核系统上都需要一个完整的内存屏障 CPU 指令。这可能成为性能瓶颈,因为它强制受影响的内存访问传播到每个核心。
此示例演示了顺序排序必要的场景。任何其他排序都可能触发断言,因为线程 c
和 d
有可能以相反的顺序观察原子 x
和 y
的更改。
#include <atomic> #include <cassert> #include <thread> std::atomic<bool> x = {false}; std::atomic<bool> y = {false}; std::atomic<int> z = {0}; void write_x() { x.store(true, std::memory_order_seq_cst); } void write_y() { y.store(true, std::memory_order_seq_cst); } void read_x_then_y() { while (!x.load(std::memory_order_seq_cst)) ; if (y.load(std::memory_order_seq_cst)) ++z; } void read_y_then_x() { while (!y.load(std::memory_order_seq_cst)) ; if (x.load(std::memory_order_seq_cst)) ++z; } int main() { std::thread a(write_x); std::thread b(write_y); std::thread c(read_x_then_y); std::thread d(read_y_then_x); a.join(); b.join(); c.join(); d.join(); assert(z.load() != 0); // will never happen }
[编辑] 与 volatile 的关系
在一个执行线程内,通过volatile glvalues的访问(读和写)不能被重新排序到同一线程内*顺序前*或*顺序后*的可观察副作用(包括其他 volatile 访问)之后,但这种顺序不保证被另一个线程观察到,因为 volatile 访问不建立线程间同步。
此外,volatile 访问不是原子的(并发读写是数据竞争),并且不进行内存排序(非 volatile 内存访问可以在 volatile 访问周围自由重新排序)。
一个值得注意的例外是 Visual Studio,在默认设置下,每个 volatile 写入都具有释放语义,每个 volatile 读取都具有获取语义(Microsoft Docs),因此 volatile 可以用于线程间同步。标准 volatile 语义不适用于多线程编程,尽管它们足以用于例如与在同一线程中运行的 std::signal 处理程序通信(当应用于 sig_atomic_t 变量时)。
[编辑] 另请参阅
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 的好参考文献。 |