[TOC]
参考
1. C++11多线程-内存模型
2. c++并发编程1.内存序
3. 浅谈Memory Reordering
4. C++11中的内存模型下篇 - C++11支持的几种内存模型
5. C++11中的内存模型上篇 - 内存模型基础
前言
有三种情况,可能导致乱序执行:编译器优化、CPU乱序、缓存不一致。进而导致多线程情况下出现问题。[1,3,4]
c++11引入了atomic类型之后,大大方便了原子变量的使用,但是原子变量的内存序有好几种,这又引入了让人难以理解的内容。
内存序分为三类六种
- relaxed(松弛的内存序)
- sequential_consistency(内存一致序)
- acquire-release(获取-释放一致性)
relaxed
//test.cpp
#include
#include
#include
std::atomic x{false},y{false};
std::atomic z{0};
void write_x_then_y() {
x.store(true,std::memory_order_relaxed); //1
y.store(true,std::memory_order_relaxed); //2
}
void read_y_then_x() {
while(!y.load(std::memory_order_relaxed)); //3
if(x.load(std::memory_order_relaxed)) //4
++z;
}
int main() {
std::thread b(read_y_then_x);
std::thread a(write_x_then_y);
a.join();
b.join();
if (z.load() != 0) return 0; else return 1;
}
# test.sh
#!/bin/bash
for ((i=0;i<1;)); do
./a.out
if [ "$?" == "1" ];then
break
fi
done
g++ -std=c++17 -pthread -O2 test.cpp
编译以上代码,time sh test.sh
执行代码。
如果出现 2 -> 3 -> 4 -> 1这样的执行次序,那么就会出现z == 0
这种错误情况。
注,不过我跑了一晚上,并没有复现这个结果
那么relaxed用于何处呢?对于计数这种场景,就可以使用relaxed来最大化性能。
#include
#include
#include
#include
std::atomic count{0};
void f() {
for (int n = 0; n < 1000; ++n) {
count.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread threads[10];
for (std::thread &thr: threads) {
thr = std::thread(f);
}
for (auto &thr : v) {
thr.join();
}
assert(cnt == 10000); // 永远不会失败
return 0;
}
release-acquire
针对relaxed的例子,如果改成如下的代码就可以避免z == 0
这种错误情况。
#include
#include
#include
std::atomic x{false},y{false};
std::atomic z{0};
void write_x_then_y() {
x.store(true,std::memory_order_relaxed); //1
y.store(true,std::memory_order_release); //2
}
void read_y_then_x() {
while(!y.load(std::memory_order_acquire)); //3
if(x.load(std::memory_order_relaxed)) //4
++z;
}
int main() {
std::thread b(read_y_then_x);
std::thread a(write_x_then_y);
a.join();
b.join();
if (z.load() != 0) return 0; else return 1;
}
他会保证1发生在2前,4发生在3后,同时3一定发生在2后,那么z == 0
不会发生。
如下图分析
- 初始条件为x = y = false。
- 在write_x_then_y线程中,先执行对x的写操作,再执行对y的写操作,由于两者在同一个线程中,所以即便针对x的修改操作使用relaxed模型,修改x也一定在修改y之前执行。
- 在write_x_then_y线程中,对y的load操作使用了acquire模型,而在线程write_x_then_y中针对变量y的读操作使用release模型,因此保证了是先执行write_x_then_y函数才到read_y_then_x的针对变量y的load操作。
- 因此最终的执行顺序如上图所示,此时不可能出现z=0的情况。
从以上的分析可以看出,针对同一个变量的release-acquire操作,更多时候扮演了一种“线程间使用某一变量的同步”作用,由于有了这个语义的保证,做到了线程间操作的先后顺序保证(inter-thread happens-before)。
可以简单记作release为写不后
,acquire为读不前
。[2]
release-consume
官方不推荐,此处不进行详细描述。简单说,release-acquire会把不相关的变量存取都进行保序,release-consume只会对有依赖的变量保序,进而提高效率,同时也使代码更容易引入bug。
sequential consistency
这是最严格的级别,也是性能最差的级别,同时也是默认的级别。
如下列:
#include
#include
#include
std::atomic x = {false};
std::atomic y = {false};
std::atomic 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)) { // 4
++z;
}
}
void read_y_then_x() {
while (!y.load(std::memory_order_seq_cst)); // 5
if (x.load(std::memory_order_seq_cst)) { // 6
++z;
}
}
int main() {
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y); // thread c
std::thread d(read_y_then_x); // thread d
a.join(); b.join(); c.join(); d.join();
// failed to assert without memory_order_seq_cst
assert(z.load() != 0);
}
如果使用release-acquire,那么线程c可能看到的是2 -> 1这个执行顺序,但是线程d可能看到的是1->2这个执行顺序,进而导致z == 0
。
如下图分析:
- 初始条件为x = y = false。
- 由于在read_x_and_y线程中,对x的load操作使用了acquire模型,因此保证了是先执行write_x函数才到这一步的;同理先执行write_y才到read_y_and_x中针对y的load操作。
- 然而即便如此,也可能出现在read_x_then_y中针对y的load操作在y的store操作之前完成,因为y.store操作与此之间没有先后顺序关系;同理也不能保证x一定读到true值,因此到程序结束是就出现了z = 0的情况。
从上面的分析可以看到,即便在这里使用了release-acquire模型,仍然没有保证z==0,其原因在于:最开始针对x、y两个变量的写操作是分别在write_x和write_y线程中进行的,不能保证两者执行的顺序导致。