定义于头文件
<atomic>
|
||
enum memory_order {
memory_order_relaxed, |
(C++11 起) (C++20 前) |
|
enum class memory_order : /*unspecified*/ {
relaxed, consume, acquire, release, acq_rel, seq_cst |
(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 修改顺序的最长相接子序列
被称为 A 所引领的释放序列。
在线程间,若下列任一为真,则求值 A 依赖先序于求值 B
在线程间,若下列任一为真,则求值 A 线程间先发生于求值 B
无关乎线程,若下列任一为真,则求值 A 先发生于求值 B :
要求实现确保先发生于关系是非循环的,若有必要则引入额外的同步(若引入消费操作,它才可能为必要,见 Batty 等)。
若一次求值修改一个内存位置,而其他求值读或修改同一内存位置,且至少一个求值不是原子操作,则程序的行为未定义(程序有数据竞争),除非这两个求值之间存在先发生于关系。
若下列皆为真,则标量 M 上的副效应 A (写入)相对于 M 上的值计算(读取)可见:
若副效应 A 相对于值计算 B 可见,则修改顺序中,满足 B 不先发生于它的对 M 的副效应的最长相接子集,被称为副效应的可见序列。( 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
,
// 线程 1 : r1 = y.load(memory_order_relaxed); // A x.store(r1, memory_order_relaxed); // B // 线程 2 : r2 = x.load(memory_order_relaxed); // C y.store(42, 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 。
即使使用宽松内存模型,也不允许“无中生有”的值循环地依赖于其各自的计算,例如,对于最初为零的 // 线程1: r1 = x.load(memory_order_relaxed); if (r1 == 42) y.store(r1, memory_order_relaxed); // 线程2: r2 = y.load(memory_order_relaxed); if (r2 == 42) x.store(42, memory_order_relaxed); 不允许产生结果 |
(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 << "Final counter value is " << cnt << '\n'; }
输出:
Final counter value is 10000
若线程 A 中的一个原子存储带标签 memory_order_release ,而线程 B 中来自同一变量的原子加载带标签 memory_order_acquire ,则从线程 A 的视角先发生于原子存储的所有内存写入(非原子及宽松原子的),在线程 B 中成为可见副效应,即一旦原子加载完成,则保证线程 B 能观察到线程 A 写入内存的所有内容。
同步仅建立在释放和获得同一原子对象的线程之间。其他线程可能看到与被同步线程的一者或两者相异的内存访问顺序。
在强顺序系统( x86 、 SPARC TSO 、 IBM 主框架)上,释放获得顺序对于多数操作是自动进行的。无需为此同步模式添加额外的 CPU 指令,只有某些编译器优化受影响(例如,编译器被禁止将非原子存储移到原子存储-释放后,或将非原子加载移到原子加载-获得前)。在弱顺序系统( ARM 、 Itanium 、 Power PC )上,必须使用特别的 CPU 加载或内存栅栏指令。
互斥锁(例如std::mutex或原子自旋锁)是释放获得同步的例子:锁被线程 A 释放且被线程 B 获得时,发生于线程 A 环境的临界区(释放之前)中的所有事件,必须对于执行同一临界区的线程 B (获得之后)可见。
#include <thread> #include <atomic> #include <cassert> #include <string> 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 <thread> #include <atomic> #include <cassert> #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; while (!flag.compare_exchange_strong(expected, 2, std::memory_order_acq_rel)) { expected = 1; } } void thread_3() { while (flag.load(std::memory_order_acquire) < 2) ; 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 ,则线程 A 视角中依赖先序于原子存储的所有内存写入(非原子和宽松原子的),会在线程 B 中该加载操作所携带依赖进入的操作中变成可见副效应,即一旦完成原子加载,则保证线程B中,使用从该加载获得的值的运算符和函数,能见到线程 A 写入内存的内容。
同步仅在释放和消费同一原子对象的线程间建立。其他线程能见到与被同步线程的一者或两者相异的内存访问顺序。
所有异于 DEC Alphi 的主流 CPU 上,依赖顺序是自动的,无需为此同步模式产生附加的 CPU 指令,只有某些编译器优化收益受影响(例如,编译器被禁止牵涉到依赖链的对象上的推测性加载)。
此顺序的典型使用情况,涉及对很少被写入的数据结构(安排表、配置、安全策略、防火墙规则等)的共时读取,和有指针中介发布的发布者-订阅者情形,即当生产者发布消费者能通过其访问信息的指针之时:无需令生产者写入内存的所有其他内容对消费者可见(这在弱顺序架构上可能是昂贵的操作)。这种场景的一个例子是 rcu 解引用。
细粒度依赖链控制可参阅 std::kill_dependency 及 [[carries_dependency]] 。
注意当前(2015年2月)没有产品编译器跟踪依赖链:消费操作被提升成获得操作。
释放消费顺序的规范正在修订中,而且暂时不鼓励使用 |
(C++17 起) |
此示例演示用于指针中介的发布的依赖定序同步:int data不由数据依赖关系关联到指向字符串的指针,从而其值在消费者中未定义。
#include <thread> #include <atomic> #include <cassert> #include <string> 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 的 memory_order_seq_cst
操作 B ,观测到下列之一:
memory_order_seq_cst
而且不先发生于 A
memory_order_seq_cst
若存在 memory_order_seq_cst
的线程栅栏( std::atomic_thread_fence )操作 X 先序于 B ,则 B 观测到下列之一:
memory_order_seq_cst
修改
设有一对 M 上的原子操作,称之为 A 和 B ,这里 A 写入、 B 读取 M 的值,若存在二个 memory_order_seq_cst
的线程栅栏( std::atomic_thread_fence ) X 和 Y ,且若 A 先序于 X , Y 先序于 B ,且 X 在单独全序中先出现于 Y ,则 B 观测到二者之一:
设有一对 M 上的原子操作,称之为 A 和 B ,若符合下列条件之一,则 M 的修改顺序中 B 先发生于 A
memory_order_seq_cst
的线程栅栏( std::atomic_thread_fence ) X ,它满足 A 先序于 X ,且 X 在单独全序中先出现于 B
memory_order_seq_cst
的线程栅栏( std::atomic_thread_fence ) Y ,它满足 Y 先序于 B ,且 A 在单独全序中先出现于 Y
memory_order_seq_cst
的线程栅栏( std::atomic_thread_fence ) X 和 Y ,它们满足 A 先序于 X , Y 先序于 B ,且 X 在单独全序中先出现于 Y
注意这表示:
memory_order_seq_cst
标签的原子操作有作用,则立即丧失序列一致性序列顺序可能在多生产者-多消费者情形下为必须,这里所有消费者必须以相同顺序观测到所有生产者的动作出现。
全序列顺序在所有多核系统上要求完全的内存栅栏 CPU 指令。这可能成为性能瓶颈,因为它强制受影响的内存访问传播到每个核心。
此示例演示序列一直顺序为必要的场合。任何其他顺序都可能触发assert,因为可能令线程c
和d
观测到原子对象x
和y
以相反顺序更改。
#include <thread> #include <atomic> #include <cassert> 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 访问前后)。
一个值得注意的例外是 Visual Studio ,其中默认设置下,每个 volatile 写拥有释放语义,而每个 volatile 读拥有获得语义( MSDN ),故而可将 volatile 对象用于线程间同步。标准的 volatile 语义不可应用于多线程编程,尽管它们在应用到 sig_atomic_t 对象时,足以与例如运行于同一线程的 std::signal 处理函数交流。
memory order的 C 文档
|
本节未完成 原因:我们在 QPI 、 MOESI 可能还有 Dragon 上找下好参考 |