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
|
具有此内存顺序的加载操作执行*获取操作*,存储操作执行*释放操作*,读-修改-写操作同时执行*获取操作*和*释放操作*,且存在一个单一全序,所有线程在其中以相同顺序观测所有修改(参见下文 顺序一致顺序 )。 |
形式化描述
线程间同步与内存排序决定了不同执行线程之间表达式的 求值 与 副作用 如何排序。它们通过以下术语进行定义:
顺序先于
在同一线程内,求值A可能 先序于 求值B,具体描述见 求值顺序 。
携带依赖关系在同一线程内,若满足以下任一条件,则先序于( sequenced-before )求值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的同一线程执行的写入操作。
|
(until C++20) |
被称为 以A为首的释放序列 。
同步关系
如果线程A中的原子存储是 释放操作 ,线程B中对同一变量的原子加载是 获取操作 ,且线程B中的加载读取到的是线程A中存储所写入的值,那么线程A中的存储 同步于 线程B中的加载。
此外,某些库调用可能被定义为与其他线程上的其他库调用 同步 。
依赖顺序先于在线程之间,若满足以下任一条件,则求值 A 依赖顺序先于 求值 B:
1)
A 对某个原子对象 M 执行
释放操作
,且在另一线程中,B 对同一原子对象 M 执行
消费操作
,并且 B 读取到由 A
所引导的释放序列中任意部分写入
(直至 C++20)
的值。
2)
A 依赖顺序先于 X,且 X 携带依赖至 B。
|
(直至 C++26) |
线程间先序关系
在线程之间,如果以下任一条件成立,则评估 A 跨线程先发生于 评估 B:
Happens-before无论线程如何,若满足以下任一条件,则求值 A happens-before 求值 B:
1)
A
sequenced-before
B。
2)
A
inter-thread happens before
B。
实现必须确保 happens-before 关系是非循环的,必要时需引入额外的同步(仅当涉及 consume 操作时才可能需要,参见 Batty et al )。 若一个求值修改内存位置,另一个求值读取或修改同一内存位置,且至少有一个求值不是原子操作,则程序的行为是未定义的(程序存在 数据竞争 ),除非这两个求值之间存在 happens-before 关系。
|
(until C++26) | ||
Happens-before无论线程如何,若满足以下任一条件,则求值 A happens-before 求值 B:
1)
A
sequenced-before
B。
2)
A
synchronizes-with
B。
3)
A
happens-before
X,且 X
happens-before
B。
|
(since C++26) |
强序先发生于
无论线程如何,若满足以下任一条件,则评估A 强发生于 评估B:
|
1)
A
sequenced-before
B。
2)
A
synchronizes-with
B。
3)
A
strongly happens-before
X,且 X
strongly happens-before
B。
|
(C++20 前) | ||
|
1)
A
sequenced-before
B。
2)
A
synchronizes with
B,且 A 和 B 均为顺序一致的原子操作。
3)
A
sequenced-before
X,X
simply
(C++26 前)
happens-before
Y,且 Y
sequenced-before
B。
4)
A
strongly happens-before
X,且 X
strongly happens-before
B。
注意:非正式地说,若 A strongly happens-before B,则 A 在所有上下文中都显得在 B 之前求值。
|
(C++20 起) |
可见副作用
标量 M 上的副作用 A(写操作)相对于 M 上的值计算 B(读操作)是 可见的 ,当且仅当以下两个条件同时成立:
如果副作用A相对于值计算B是可见的,那么在 修改顺序 中,对M的副作用的最长连续子集(其中B不 先发生于 该子集)被称为 副作用的可见序列 (由B确定的M值,将是这些副作用之一所存储的值)。
注意:线程间同步的核心在于防止数据竞争(通过建立先发生关系)以及定义在何种条件下哪些副作用变得可见。
Consume 操作
使用
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(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 == 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 之前,规范在技术上允许这种情况,但不建议实现者这样做。 |
(since 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"); // 永远不会触发 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) ; // 如果从原子标志读取到值 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指令,仅影响特定编译器优化(例如禁止编译器对依赖链涉及的对象执行推测性加载)。
此排序模式的典型用例包括:对少写入的并发数据结构(路由表、配置、安全策略、防火墙规则等)的读取访问,以及通过指针传递发布的发布-订阅场景(即生产者发布指针,消费者通过该指针访问信息时,无需使生产者写入内存的其他内容对消费者可见——这在弱序架构上可能是高成本操作)。此类场景的实例包括
细粒度依赖链控制请参阅
std::kill_dependency
及
注意当前(2015年2月)尚无已知的生产编译器跟踪依赖链:消费操作均被提升为获取操作。 |
(直至C++26) |
|
释放-消费顺序的规范正在修订中,暂不鼓励使用
|
(since C++17)
(until C++26) |
|
释放-消费顺序与释放-获取顺序具有相同效果,现已被弃用。 |
(since 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 加载的
若存在一个
顺序先于
B 的
对于 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 上更高效地实现
例如,在
// 线程 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,但 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
语义不适用于多线程编程,但当应用于
sig_atomic_t
变量时,它们足以实现与运行在同一线程中的
std::signal
处理程序之间的通信。编译器选项
/volatile:iso
可用于恢复符合标准的行为,这在目标平台为ARM时是默认设置。
参见
|
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 |
|
本节内容尚不完整
原因:需要寻找关于QPI、MOESI及可能Dragon协议的高质量参考文献。 |