std::memory_order
在头文件中定义 <atomic> |
||
enum memory_order { |
(自 C++11 起) (直到 C++20) |
|
enum class memory_order : /* unspecified */ { |
(自 C++20 起) | |
std::memory_order
指定了如何在原子操作周围对内存访问(包括常规的非原子内存访问)进行排序。在没有对多核系统的任何约束的情况下,当多个线程同时读写多个变量时,一个线程可能会观察到值的更改顺序与另一个线程写入它们的顺序不同。实际上,更改的明显顺序甚至可能在多个读取线程之间有所不同。由于编译器允许内存模型进行的转换,即使在单处理器系统上也会发生一些类似的效果。
库中所有原子操作的默认行为提供顺序一致排序(参见下面的讨论)。该默认值可能会影响性能,但库的原子操作可以指定一个额外的std::memory_order
参数来指定除了原子性之外的编译器和处理器必须为此操作强制执行的确切约束。
内容 |
[编辑] 常量
在头文件中定义
<atomic> | |
名称 | 解释 |
memory_order_relaxed
|
松弛操作:没有对其他读取或写入施加任何同步或排序约束,只有此操作的原子性得到保证(参见下面的松弛排序)。 |
memory_order_consume
|
具有此内存顺序的加载操作对受影响的内存位置执行消费操作:当前线程中对当前加载值有依赖关系的任何读取或写入都不能在该加载之前重新排序。在其他线程中对依赖于数据的变量的写入,这些写入释放相同的原子变量,在当前线程中是可见的。在大多数平台上,这仅影响编译器优化(参见下面的释放-消费排序)。 |
memory_order_acquire
|
具有此内存顺序的加载操作对受影响的内存位置执行获取操作:当前线程中的任何读取或写入都不能在该加载之前重新排序。在其他线程中释放相同原子变量的所有写入在当前线程中是可见的(参见下面的释放-获取排序)。 |
memory_order_release
|
具有此内存顺序的存储操作执行释放操作:当前线程中的任何读取或写入都不能在该存储之后重新排序。当前线程中的所有写入对获取相同原子变量的其他线程是可见的(参见下面的释放-获取排序),并且将依赖关系引入原子变量的写入对消费相同原子的其他线程是可见的(参见下面的释放-消费排序)。 |
memory_order_acq_rel
|
具有此内存顺序的读-修改-写操作既是获取操作又是释放操作。当前线程中的任何内存读取或写入都不能在加载之前重新排序,也不能在存储之后重新排序。在其他线程中释放相同原子变量的所有写入在修改之前是可见的,并且修改对获取相同原子变量的其他线程是可见的。 |
memory_order_seq_cst
|
具有此内存顺序的加载操作执行获取操作,存储执行释放操作,读-修改-写操作既执行获取操作又执行释放操作,此外,还存在一个唯一的总顺序,所有线程以相同的顺序观察所有修改(参见下面的顺序一致排序)。 |
[编辑] 形式化描述
线程间同步和内存排序决定了表达式的 *评估* 和 *副作用* 在不同执行线程之间的顺序。它们在以下术语中定义
[编辑] 顺序-前
在同一个线程中,评估 A 可以在评估 B *之前排序*,如 评估顺序 所述。
[编辑] 携带依赖
在同一个线程中,评估 A 在评估 B *之前排序* 也可能将依赖关系传递到 B(即 B 依赖于 A),如果以下任何一项为真
[编辑] 修改顺序
对任何特定原子变量的所有修改都发生在一个总顺序中,该顺序特定于此单个原子变量。
以下四个要求适用于所有原子操作
[编辑] 释放序列
在原子对象 M 上执行 *释放操作* A 后,M 的修改顺序中最长的连续子序列,它包含
1) 由执行 A 的相同线程执行的写入。
|
(直到 C++20) |
被称为 *以 A 为首的释放序列*。
[编辑] 与…同步
如果线程 A 中的原子存储是 *释放操作*,线程 B 中从相同变量的原子加载是 *获取操作*,并且线程 B 中的加载读取了线程 A 中存储写入的值,那么线程 A 中的存储 *与* 线程 B 中的加载 *同步*。
此外,某些库调用可能被定义为 *与* 其他线程上的其他库调用 *同步*。
[编辑] 依赖-有序-前
在线程之间,如果以下任何一项为真,则评估 A *依赖-有序-前* 评估 B
[编辑] 线程间发生-前
在线程之间,如果以下任何一项为真,则评估 A *线程间发生-前* 评估 B
[编辑] 发生-前
无论线程如何,如果以下任何一项为真,则评估 A *发生-前* 评估 B
实现需要确保 *发生-前* 关系是无环的,如果需要,通过引入额外的同步(只有在使用操作时才可能需要,请参阅 Batty 等人)。
如果一个评估修改了内存位置,而另一个评估读取或修改了相同的内存位置,并且如果至少一个评估不是原子操作,那么程序的行为是未定义的(程序具有 数据竞争),除非在这两个评估之间存在 *发生-前* 关系。
简单的发生-前无论线程如何,如果以下任何一项为真,则评估 A *简单发生-前* 评估 B 1) A *顺序-前* B。
2) A *与* B *同步*。
3) A *简单发生-前* X,而 X *简单发生-前* B。
注意:没有使用操作,*简单发生-前* 和 *发生-前* 关系是相同的。 |
(自 C++20 起) |
[编辑] 强发生-前
无论线程如何,如果以下任何一项为真,则评估 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 *简单发生-前* Y,而 Y *顺序-前* B。
4) A *强发生-前* X,而 X *强发生-前* B。
注意:非正式地,如果 A *强发生-前* B,那么 A 似乎在所有上下文中都在 B 之前被评估。 注意:*强发生-前* 排除了使用操作。 |
(自 C++20 起) |
[编辑] 可见副作用
如果以下两项都为真,则标量 M 上的副作用 A(写入)对于 M 上的值计算 B(读取)是 *可见的*
如果副作用 A 相对于值计算 B 是可见的,那么 M 的副作用的最长连续子集,在 *修改顺序* 中,其中 B 不 *发生-前* 它被称为 *可见副作用序列*(由 B 确定的 M 的值将是这些副作用之一存储的值)。
注意:线程间同步归结为防止数据竞争(通过建立发生-前关系)并定义哪些副作用在哪些条件下变得可见。
[编辑] 使用操作
具有 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,因为,尽管 A *顺序-前* B 在线程 1 中,而 C *顺序-前* D 在线程 2 中,但没有任何东西可以阻止 D 在 y 的修改顺序中出现在 A 之前,而 B 在 x 的修改顺序中出现在 C 之前。D 对 y 的副作用可能对线程 1 中的加载 A 可见,而 B 对 x 的副作用可能对线程 2 中的加载 C 可见。特别是,这可能发生在 D 在线程 2 中的 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 时,才能将 42 存储到 y,而这又循环依赖于将 42 存储到 y。请注意,在 C++14 之前,从规范上来说,这种情况 technically allowed,但并不推荐实现者这样做。 |
(从 C++14 开始) |
放松内存排序的典型用法是递增计数器,例如 std::shared_ptr 的引用计数器,因为这只需要原子性,而不需要排序或同步(请注意,递减 std::shared_ptr
计数器需要与析构函数进行 acquire-release 同步)。
#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
[edit] Release-Acquire 排序
如果线程 A 中的原子存储被标记为 memory_order_release,而线程 B 中对同一变量的原子加载被标记为 memory_order_acquire,并且加载操作在线程 B 中读取了线程 A 存储的值,那么线程 A 中的存储操作将与线程 B 中的加载操作 _同步_。
从线程 A 的角度来看,所有在原子存储之前 _发生_ 的内存写入(包括非原子和放松的原子操作),都将成为线程 B 中的 _可见副作用_。也就是说,一旦完成原子加载,线程 B 保证能看到线程 A 写入内存的所有内容。此承诺只有在 B 实际返回 A 存储的值,或返回 release 序列中后面的值时才成立。
同步只在 _释放_ 和 _获取_ 同一原子变量的线程之间建立。其他线程看到的内存访问顺序可能与这两个同步线程中的一个或两个都不一样。
在强排序系统(例如 x86、SPARC TSO、IBM 大型机等)上,release-acquire 排序对于大多数操作来说是自动的。此同步模式不会发出额外的 CPU 指令,只会影响某些编译器优化(例如,编译器禁止将非原子存储移动到原子存储-release 之后,或者将非原子加载执行到原子加载-acquire 之前)。在弱排序系统(例如 ARM、Itanium、PowerPC)上,会使用特殊的 CPU 加载或内存栅栏指令。
互斥锁(例如 std::mutex 或 原子自旋锁)就是 release-acquire 同步的一个例子:当线程 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(); }
以下示例演示了跨三个线程的传递式 release-acquire 排序,使用 release 序列。
#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(); }
[edit] Release-Consume 排序
如果线程 A 中的原子存储被标记为 memory_order_release,而线程 B 中对同一变量的原子加载被标记为 memory_order_consume,并且加载操作在线程 B 中读取了线程 A 存储的值,那么线程 A 中的存储操作将 _依赖于_ 线程 B 中的加载操作。
从线程 A 的角度来看,所有在原子存储之前 _发生_ 的内存写入(非原子和放松的原子操作),都将成为线程 B 中那些 _依赖于_ 加载操作的运算和函数的 _可见副作用_,也就是说,一旦完成原子加载,线程 B 中那些使用从加载操作中获取的值的运算符和函数,都保证能看到线程 A 写入内存的内容。
同步只在 _释放_ 和 _使用_ 同一原子变量的线程之间建立。其他线程看到的内存访问顺序可能与这两个同步线程中的一个或两个都不一样。
除了 DEC Alpha 之外,所有主流 CPU 上的依赖排序都是自动的,此同步模式不会发出额外的 CPU 指令,只会影响某些编译器优化(例如,编译器禁止对参与依赖链的对象执行推测性加载)。
此排序的典型用例包括对很少写入的并发数据结构(路由表、配置、安全策略、防火墙规则等)的读访问,以及使用指针进行发布的发布者-订阅者场景,也就是说,当生产者通过指针发布订阅者可以访问的信息时:不需要让生产者写入内存的所有其他内容对订阅者可见(这在弱排序架构上可能是一个昂贵的操作)。此类场景的一个例子是 rcu_dereference
。
另请参见 std::kill_dependency 和 [[carries_dependency]]
,以实现对依赖链的细粒度控制。
请注意,目前(2015 年 2 月)已知的生产编译器都没有跟踪依赖链:使用操作会被提升为获取操作。
release-consume 排序的规范正在修订,暂时不建议使用 |
(从 C++17 开始) |
此示例演示了指针进行发布的依赖排序同步:整型数据与指向字符串的指针之间不存在数据依赖关系,因此其值在消费者中是未定义的。
#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(); }
[edit] 顺序一致排序
标记为 memory_order_seq_cst 的原子操作不仅以与 release/acquire 排序相同的方式对内存进行排序(在某个线程中 _发生_ 在存储操作之前的任何内容都将成为执行加载操作的线程中的 _可见副作用_),还会为所有如此标记的原子操作建立一个 _唯一的总修改顺序_。
形式上, 每个从原子变量 M 加载的
如果有
对于对 M 进行的两个原子操作 A 和 B,其中 A 写入,而 B 读取 M 的值,如果存在两个
对于对 M 进行的两个原子修改 A 和 B,如果 B 在 M 的修改顺序中出现在 A 之后,则
请注意,这意味着 1) 一旦未标记为
memory_order_seq_cst 的原子操作出现,顺序一致性就会丢失,2) 顺序一致栅栏只为栅栏本身建立总排序,在一般情况下不会为原子操作建立总排序(_排序_ 不是跨线程关系,而 _发生_ 是)。 |
(直到 C++20) |
形式上, 如果以下情况之一成立,那么对某个原子对象 M 进行的原子操作 A 将 _一致排序_ 在对 M 进行的另一个原子操作 B 之前 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 上更有效地实现 例如,如果 // 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 的良好参考资料。 |