前言
原子变量是C++解决多线程中数据竞争、线程同步的一种方式,而内存序(Memory Order)则是使用原子变量前的一个重要知识。
在Luyu Huang’s Blog的文章中,已经对C++的内存序做出了比较清晰易懂的解释,本文的目的旨在提炼其文章中的核心思想,补充其文章中所提到的不足和发表一点作者自己的看法与感想。
原子变量
在使用原子变量时,有load、store、rmw三种基础操作,每次操作原子变量前,我们都需要指定其使用的内存序(某些函数有默认的内存序,比如load和store),在C++中一共规定了六种内存序,分别对应三种内存模型:
Sequencial Consistent模型
memory_order_seq_cst
Relaxed模型memory_order_relaxed
Acquire-release模型memory_order_acquire
memory_order_release
memory_order_acq_rel
Acuqire-release.Comsume-release模型memory_order_consume
要了解这六种内存序,我们先从一些基本概念开始说起。
修改顺序一致性
同一原子变量能保证修改顺序在所有线程中的一致性,这意味着:
- 两个修改操作之间一定存在先后顺序
- 修改后的值立即可见且各线程所看见的值相同
Happens-before关系
Happens-before关系描述的是操作A、B之间,如果操作A happens-before 操作B,那么操作A的结果对操作B可见。
单线程中的happens-before关系
在单线程中,指令总是顺序执行的,前面的操作总是比后面的操作先执行,这称为sequenced-before关系[0]。由这一特点我们可知,如果操作A、B之间具有sequenced-before关系,则A、B之间构成happens-before关系。
sequenced-before关系同样具有传递性:如果a sequenced-before k,k sequenced-before b,则a sequenced-before b。
多线程中的happens-before关系
在多线程中,为了构成happens-before关系,我们先引入新的两种关系:synchronizes-with关系和inter-thread happens-before关系。
首先,如果a inter-thread happens-before b,那么a happens-before b
并且synchronizes-with关系可以用来构建inter-thread happens-before关系。
要构建A inter-thread happens before B,则:
- A synchronizes-with B 或者
- A inter-thread happens-before k, k synchronizes-with B 或者
- A sequenced-before k, k inter-thread happens-before B 或者
- A inter-thread happens-before k, k inter-thread happens-before B
而synchronizes-with关系,则由下文所述的内存顺序模型提供。
Happens-before不代表指令实际的执行顺序
happens-before是 C++ 语义层面的概念, 它并不代表指令在 CPU 中实际的执行顺序。 编译器可能会在不破坏语义的前提下对指令重排。
六种内存序与四种内存顺序模型
顺序一致性(Sequencial Consistent)模型
这种模型所对应的内存序为memory_order_seq_cst
,可以用于load, store和RMW操作。seq_cst这一内存序保证了对所有在顺序一致性模型下的原子变量,其修改顺序具有一致性。
例如,对于变量x的操作A和对变量y的操作B在这一模型下不可能同时发生,并且A、B的修改顺序能被所有线程看到,而且所看到的顺序一致[1]。
顺序一致性模型可以构建synchronizes-with关系:在seq_cst下的load操作读到seq_cst下的store操作写入的值,则这一load操作 synchronizes-with 这一store操作。
宽松(Relaxed)模型
这一模型所对应的内存序为memory_order_relaxed
,可以用于load, store和RMW操作。relaxed这一内存序对原子变量的行为约束最少,其只能保证对原子变量操作的原子性和修改顺序的一致性,无法用于构建synchronizes-with关系。
Acquire-release模型
这一模型对应着三种内存序:memory_order_acquire
, memory_order_release
和memory_order_acq_rel
。
memory_order_acquire
用于load操作,称为acquire操作memory_order_release
用于store操作,称为release操作memory_order_acq_rel
用于RMW操作。同时,如果RMW操作使用memory_order_acquire
,则作为acquire操作;使用memory_order_release
,则作为release操作
acquire-release模型可以用于构建synchronizes-with关系:
- 如果一个acquire操作读取到了一个release操作写入的值,则这个release操作 synchronizes-with 这个acquire操作。
- 如果一个acquire操作读取到了一个以某一release操作为首的release sequence[2]写入的值,则这个release操作 synchronizes-with 这个acquire操作。
acquire-release模型所保证的顺序一致性是对于单一原子变量而言的,相较于sequencial-consistent模型而言,其缺少了对所有原子变量的约束,这意味着对于不同的两个原子变量而言,它们的行为可以同时发生。
Consume-release模型
comsume-release模型是acquire-release的一种特殊形式,其所对应的内存序为:memory_order_consume
,用于load操作。为了理解这一模型,我们要引入两个新的概念:Carries dependency和Dependency-ordered before。
carries dependency描述的是在两个具有sequenced-before的关系之间(A sequenced-before B)的关系,如果:
- A 是 B的一个操作数;或者
- A写入了某个标量M,而B要从这个标量M中读取数据;或者
- X依赖A, B依赖X
那么我们称B依赖A,或者说A carries a dependency into B
如果一个comsume load读到了一个release操作或者release sequence headed by这个release操作写入的值,那么这个comsume load dependency-ordered before这个release操作。
与inter-thread happens before关系类似,dependency-ordered before关系可以通过carries dependency关系来进行“桥接”:
- 如果A dependency-ordered before k, k carries a dependency into B,则A dependency-ordered before B
需要注意的是,memory_order_consume
这一内存序比较复杂,某些编译器上没有给出实现,这个时候memory_order_consume
等同于memory_order_acquire
。
这里对上文进行补充:comsume load与release操作之间构成的dependency-ordered before关系可以构成inter-thread happens before关系,进而构成happens-before关系。
happens-before关系构成方式的总结
- 单独的sequenced-before关系、inter-thread happens before关系和dependency-ordered before关系可以构成happens-before关系,且前两种关系具有传递性。
- 对于一组synchronizes-with关系,可以后接sequenced-before关系构成inter-thread happens before关系,进而构成happens-before关系。
- 对于一组dependency-ordered before关系,可以后接carries dependency关系扩展dependency-ordered before关系。
- 而inter-thread happens before关系又可以前接sequenced-before关系,或者后接sychronizes-with关系进行扩展。
图源:Luyu Huang’s Blog——谈谈 C++ 中的内存顺序 (Memory Order)
不同内存序分析与应用举例
Relaxed内存序
看下面这段代码:
// Thread 1:
v1 = y.load(std::memory_order_relaxed); // 1
x.store(v1, std::memory_order_relaxed); // 2
// Thread 2:
v2 = x.load(std::memory_order_relaxed); // 3
y.store(1, std::memory_order_relaxed); // 4
由于这段代码的所有操作均使用memory_order_relaxed
这一内存序,所以对于原子变量的操作只能保证原子性和修改顺序的一致性。
由于Relaxed内存序仅保证原子性,所以程序执行是无序的;而修改顺序一致性则确保了1和4,2和3不可能同时执行。
在这种内存序下,v1==v2==1
是被允许的(例如程序按照3412的顺序执行)。
Acquire-release内存序
先看一段程序:
std::atomic<bool> x{false}, y{false};
void thread1() {
x.store(true, std::memory_order_relaxed); // 1
y.store(true, std::memory_order_release); // 2
}
void thread2() {
while (!y.load(std::memory_order_acquire)); // 3
assert(x.load(std::memory_order_relaxed)); // 4
}
现在我们思考:断言4会不会失败?
我们先分析一下:在这段程序中,3能够读取到2所写入的数据,4能够读取到1所写入的数据,所以:
- 2 synchronizes-with 3
- 1 synchronizes-with 4
并且由于1 sequenced before 2,3 sequenced before 4
所以2 inter-thread happens before 4,可以推出1 inter-thread happens before 4,即1 happens before 4
所以在1的结果一定对4可见,即x开始读取时,已经被设置为true,故断言不会失败。
Consume-release内存序
std::atomic<std::string*> ptr;
int data;
void thread1() {
std::string* p = new std::string("Hello"); // (1)
data = 42; // (2)
ptr.store(p, std::memory_order_release); // (3)
}
void thread2() {
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_consume))); // (4)
assert(*p2 == "Hello"); // (5)
assert(data == 42); // (6)
}
分析程序可知:
- consume load (4) 读取到 release (3),所以(3) dependency-ordered before (4)
- p2是(5)的操作数,则 (4) carries a dependency into (5)
- 所以(3) inter-thread happens before (5)
- 又因为(1) sequenced-before (3),所以 (1) inter-thread happens before (5)
所以(5)一定能读到(1)写入的值,(5)不会失败;但(3)和(6)之间没有happens before关系(虽然(3) happens before【dependency-ordered before】 (4),(4) happens before 【sequenced before】 (6),但happens before没有传递性,且(3)、(4)、(6)之间的关系无法组合成一个心得关系构成happens before关系),所以断言(6) 不一定成功。
Sequencial Consistent内存序
示例程序如下:
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); // 1
}
void write_y()
{
y.store(true, std::memory_order_seq_cst); // 2
}
void read_x_then_y()
{
while (!x.load(std::memory_order_seq_cst)); // 3
if (y.load(std::memory_order_seq_cst)) ++z; // 4
}
void read_y_then_x()
{
while (!y.load(std::memory_order_seq_cst)); // 5
if (x.load(std::memory_order_seq_cst)) ++z; // 6
}
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); // 7
}
现在我们同样思考一个问题:断言7会不会失败?
要想断言7失败,那么我们必须满足使得z=0
,而要使得z=0
,必须同时满足:
- read_x_then_y()获取到x=true,y=false;
- read_y_then_x()获取到y=true,x=false。
并且由于seq_cst模型的约束,1、2、3、4、5、6之间也存在一个顺序。如果read_x_then_y()先获取到x=true,那么必然意味着1已经被执行,当read_y_then_x()获取到y=true时,由于这一顺序的存在,此时x必定为true,z++执行,反之同理。
所以上述断言失败的条件不成立,所以断言不会失败。
同时我们分析不难得到:
- 1 sychronizes-with 3,6
- 2 sychronizes-with 4,5
- 3 sequenced before 4
- 5 sequenced before 6。
所以可以得到:
- 1 inter-thread happens before 4
- 2 inter-thread happens before 6
内存序对读/写顺序的作用
- 宽松内存序对同一原子变量的读写几乎无约束作用,仅保证读写操作的原子性和修改顺序的一致性
- Acquire-release内存序会在读取数据时禁止其前的写操作排到该读操作的后面,写入数据时禁止其前的读操作排到该写操作的后面,在RMW时则同时禁止。
- 而Seq_cst内存序则是在Acquire-release内存序的基础上,对所有原子变量进行了约束,对某一原子变量操作后的其他操作,包括对其他原子变量的操作不能在这一操作前,其前的操作不能在这一操作后。
补充概念的严谨定义
Sequenced before关系
1. 定义
sequenced before关系描述的是同一线程中的求值之间的非对称的、传递的对偶关系,具体为:
- A sequenced before B,则A的求值在B之前完成
- A 不sequenced before B,B sequenced before A,则B的求值在A之前完成
- 如果A既不sequenced before B,B也不sequenced before A,那么A、B之间的求值顺序无序或者顺序不定。
注:无序,意味着操作可以同时发生,且顺序可以随意互换;顺序不定,仅意味着操作之间的顺序可以互换,但不能同时发生
2. 关系的确立规则
确定sequenced before的规则多达20条,这里就以截图的形式展示了,读者可以自行查阅cppreference.com(见参考)。
Happens-before与可见性
写A的结果要被读B看见则必须满足下面的条件:
- A happens before B;或
- A happens before X,X happens before B,并且X对A要写入的值无副作用
注:函数的除产生返回值以外对调用方的其他作用即为副作用,例如修改全局变量、IO操作等。
Modification Order
1. 定义
对于同一个原子变量的所有修改操作,均在特定于这个原子变量的全序上发生。
2. 性质
这一定义保证原子变量在多个线程中,对于操作A happens before 操作B:
- A不可能读到B写入的值;或者
- A一定在B写入之前写入;或者
- A写入的值有可能被B读到,否则B读取的值一定是A之后的操作写入的值;或者
- A读取到的值X可能也被B读取,否则B读取的值一定比X更新
Release sequence
1. 定义
原子变量M上的release sequence headed by A指的是M的修改顺序上的一个最大连续子序列,以写操作A开始的,当前线程对M的写操作(C++20后被废弃),其他线程对M的RMW操作构成的。
2. 解释
意思就是:操作A和其后的所有连续写和RMW操作构成一个release sequence
比如序列(R表示读,W表示写,RMW表示读-修改-写操作,t表示线程):R R W1 R WA t2-RMW1 W2 R RMW2 R
release headed by A指的就是WA t2-RMW1 W2这一序列(C++20前)或WA t2-RMW1这一序列(C++20及以后)
内存序与底层执行
需要注意的是,内存序是C++语言层面上的概念,它制约着编译器的优化和行为,保障程序执行的语义与使用内存序所期待的语义一致。换言之,我们在C++中所要求的内存序与底层真正执行的指令的顺序是可能不相同的,只是指令执行所展现出来的效果与我们所想要达到的效果一致。
总结
从约束能力上来说,memory_order_seq_cst
最严格,约束能力最强,但性能损失也是最大的,memory_order_relax
最宽松,约束能力最弱,性能损失最小;而memory_order_acquire
、memory_order_release
、memory_order_acq_rel
和memory_order_consume
的性能损失和约束能力则介于这两者之间。
从作用范围来说,只有memory_order_seq_cst
对全局有作用,剩余的内存序均只作用于某一个原子变量。
相关阅读
参考
C++ memory order循序渐进(二)—— C++ memory order基本定义和形式化描述所需术语关系详解