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
|
有此内存定序的加载操作,在其影响的内存位置进行消费操作:当前线程中依赖于当前加载的值的读或写不能被重排到此加载之前。其他线程中对有数据依赖的变量进行的释放同一原子变量的写入,能为当前线程所见。在大多数平台上,这只影响到编译器优化(见下方释放-消费定序)。 |
memory_order_acquire
|
有此内存定序的加载操作,在其影响的内存位置进行获得操作:当前线程中读或写不能被重排到此加载之前。其他线程的所有释放同一原子变量的写入,能为当前线程所见(见下方释放-获得定序)。 |
memory_order_release
|
有此内存定序的存储操作进行释放操作:当前线程中的读或写不能被重排到此存储之后。当前线程的所有写入,可见于获得该同一原子变量的其他线程(见下方释放-获得定序),并且对该原子变量的带依赖写入变得对于其他消费同一原子对象的线程可见(见下方释放-消费定序)。 |
memory_order_acq_rel
|
带此内存定序的读修改写操作既是获得操作又是释放操作。当前线程的读或写内存不能被重排到此存储之前或之后。所有释放同一原子变量的线程的写入可见于修改之前,而且修改可见于其他获得同一原子变量的线程。 |
memory_order_seq_cst
|
有此内存定序的加载操作进行获得操作,存储操作进行释放操作,而读修改写操作进行获得操作和释放操作,再加上存在一个单独全序,其中所有线程以同一顺序观测到所有修改(见下方序列一致定序)。 |
正式描述
线程间同步和内存定序决定了表达式的求值 和副作用 如何在不同的执行线程间排序。它们用下列术语定义:
先序于
在同一线程中,求值 A 可以先序于求值 B,如求值顺序中所描述。
携带依赖
在同一线程中,若下列任一为真,则先序于求值 B 的求值 A 可能也会将依赖带入 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 上的值计算(读取)可见:
如果副作用 A 相对于值计算 B 可见,那么按 修改顺序 排列的 M 的且不 先发生于 B 的最长连续副作用子集,称为副作用的可见序列(由 B 确定的 M 值将是这些副作用之一存储的值)。
注意:线程间同步可归结为避免数据竞争(通过建立先发生于关系),及定义在何种条件下哪些副作用成为可见。
消费操作
带 memory_order_consume
或更强标签的原子加载是消费操作。注意 std::atomic_thread_fence 会施加比消费操作更强的同步要求。
获得操作
带 memory_order_acquire
或更强标签的原子加载是获得操作。互斥上的 lock()
操作亦为获得操作。注意 std::atomic_thread_fence 会施加比获得操作更强的同步要求。
释放操作
带 memory_order_release
或更强标签的原子存储是释放操作。互斥上的 unlock()
操作亦为释放操作。注意 std::atomic_thread_fence 会施加比释放操作更强的同步要求。
解释
宽松定序
被标以 memory_order_relaxed 的原子操作不是同步操作;它们不会为并发的内存访问行为添加定序约束。它们只保证原子性和修改顺序的一致性。
例如,对于初始值为零的 x
和 y
,
// 线程 1: r1 = y.load(std::memory_order_relaxed); // A x.store(r1, std::memory_order_relaxed); // B // 线程 2: r2 = x.load(std::memory_order_relaxed); // C y.store(42, std::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 之前完成的情况下发生,无论因为编译器重排还是发生于运行时。
即使使用宽松内存模型,也不允许“无中生有”的值循环地依赖于其各自的计算,例如,对于初始值为零的 x 和 y, // 线程1: r1 = y.load(std::memory_order_relaxed); if (r1 == 42) x.store(r1, std::memory_order_relaxed); // 线程2: r2 = x.load(memory_order_relaxed); if (r2 == 42) y.store(42, std::memory_order_relaxed); 不会出现 r1 == r2 == 42,因为向 y 存储 42 只会在向 x 存储的值是 42 时才可能发生,而这循环依赖于向 y 所存储的值为 42,这被规范所允许但并不建议实现厂商如此做。 |
(C++14 起) |
宽松内存定序的典型的应用是计数器自增,例如 std::shared_ptr 的引用计数器,因为这只要求原子性,但不要求定序或同步(注意 std::shared_ptr
计数器的自减要求与析构函数间进行获得-释放同步)。
#include <vector> #include <iostream> #include <thread> #include <atomic> 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 << "最终计数器值为 " << cnt << '\n'; }
输出:
最终计数器值为 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、Power PC)上,必须使用特别的 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"); // 绝无问题 assert(data == 42); // 绝无问题 } 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 是可以的,因为这是一个 RMW 操作 // 而 RMW(以任意定序)跟在释放之后将组成释放序列 while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) { expected = 1; } } void thread_3() { while (flag.load(std::memory_order_acquire) < 2) ; // 如果我们从 atomic flag 中读到 2,将看到 vector 中储存 42 assert(data.at(0) == 42); //决不出错 } 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 指令,只有某些编译器优化会受影响(例如,编译器被禁止牵涉到依赖链的对象上的推测性加载)。
此定序的典型使用情况,包括对很少被写入的并发数据结构(路由表、配置、安全策略、防火墙规则等)的读取访问,和有指针中介发布的发布者-订阅者的情形,即生产者所发布的指针,消费者能通过其访问信息:无需令生产者写入内存的所有其他内容对消费者可见(这在弱顺序架构上可能是昂贵的操作)。这种场景的例子之一是 rcu_dereference
。
细粒度依赖链控制可参阅 std::kill_dependency 及 [[carries_dependency]]
。
注意到 2015 年 2 月为止没有任何已知产品级编译器跟踪依赖链:消费操作均被提升为获得操作。
释放消费定序的规范正在修订中,而且暂时不鼓励使用 |
(C++17 起) |
此示例演示用于指针中介的发布的依赖定序同步:int data
不由数据依赖关系关联到指向字符串的指针,从而其值在消费者中未定义。
#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"); // 绝无出错: *p2 从 ptr 携带依赖 assert(data == 42); // 可能也可能不会出错: data 不从 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,若符合下列条件之一,则 M 的修改顺序中 B 先发生于 A:
注意这表明: 1) 一旦出现未标记
memory_order_seq_cst 的原子操作,则立即丧失序列一致性,2) 序列一致栅栏仅为栅栏自身建立全序,而不为通常情况下的原子操作建立(先序于 不是跨线程关系,不同于先发生于) |
(C++20 前) |
正式而言,
某原子对象 M 上的原子操作连贯先序于 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 操作,则 S 中 A 前于 B,b) 若 A 是
memory_order_seq_cst 操作,而 B 先发生于 memory_order_seq_cst 栅栏 Y,则 S 中 A 前于 Y,c) 若
memory_order_seq_cst 栅栏 X 先发生于 A,而 B 为 memory_order_seq_cst 操作,则 S 中 X 前于 B,d) 若
memory_order_seq_cst 栅栏 X 先发生于 A,而 B 先发生于 memory_order_seq_cst 栅栏 Y,则 S 中 X 前于 Y。正式定义确保: 1) 单独全序与任何原子对象的修改顺序一致。
2)
memory_order_seq_cst 加载到的值,要么来自最后一次 memory_order_seq_cst 修改,要么来自某个不先发生于顺序中之前的 memory_order_seq_cst 修改操作的非 memory_order_seq_cst 修改。单独全序可能与先发生于不一致。这允许 例如,对于初值为零的 // 线程 1 : x.store(1, std::memory_order_seq_cst); // A y.store(1, std::memory_order_release); // B // 线程 2 : r1 = y.fetch_add(1, std::memory_order_seq_cst); // C r2 = y.load(std::memory_order_relaxed); // D // 线程 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,但 注意: 1) 一旦出现未标记
memory_order_seq_cst 的原子操作,程序的序列一致保证就会立即丧失,2) 多数情况下, memory_order_seq_cst 原子操作相对于同一线程所进行的其他原子操作可重排。 |
(C++20 起) |
在多生产者-多消费者的情形中,若所有消费者都必须以相同顺序观察到所有生产者的动作出现,则可能必须进行序列定序。
全序列定序在所有多核系统上都要求完全的内存栅栏 CPU 指令。这可能成为性能瓶颈,因为它强制受影响的内存访问传播到每个核心。
此示例演示序列一致定序为必要的场合。任何其他定序都可能触发 assert,因为可能令线程 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); //决不发生 }
与 volatile 的关系
在执行线程中,不能将通过 volatile 泛左值进行的访问(读和写)重排到同线程内先序于或后序于它的可观测副效应(包含其他 volatile 访问)后,但不保证另一线程观察到此顺序,因为 volatile 访问不建立线程间同步。
另外,volatile 访问不是原子的(共时的读和写是数据竞争),且没有内存定序(非 volatile 内存访问可以自由地重排到 volatile 访问前后)。
一个值得注意的例外是 Visual Studio,其中默认设置下,每个 volatile 写拥有释放语义,而每个 volatile 读拥有获得语义(微软文档),故而可将 volatile 对象用于线程间同步。标准的 volatile 语义不可应用于多线程编程,尽管它们在应用到 sig_atomic_t 对象时,足以用于例如与运行于同一线程的 std::signal 处理函数间的通信。
参阅
外部链接
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 上找到好的参考资料。 |