C++11多线程 内存序(std::memory_order_relaxed)
目录
引言
cpu架构
std::memory_order_relaxed(宽松内存序)介绍
示例代码
引言
本文是讲解C++内存序一列文章中的一部分,主要讲解宽松内存序的理解和使用。
本部分将从如下三方面讲解:
- 一个粗略的可能存在的现代cpu架构
- 宽松内存序介绍
- 示例代码(主要来自《C++ Concurrency in Action》)
关于内存模型相关知识可参考C++多线程 内存序(顺序一致性)
cpu架构
一个粗略的现代cpu架构可能如下所示
上述提供了一个粗略的现代CPU架构,上述中CPU标注的块,代表着一个Core,此处说明一下。
在上述4core系统中,每两个core构成一个bank,并共享一个cache,且每个core均有一个store buffer。
本文给出的多核结构仅仅为了理解std::memory_order_relaxed作为基础,所以有些解释非官方,譬如我们假设,每个CPU所作的store均会写到store buffer中,关于何时写入到cache甚至memory不再我们的考虑范围之内,只需要知道每个CPU会在任何时刻将store buffer中结果写入到cache或者memory中。
关于cache之间的一致性协议不再本文讲述范围内,感兴趣的可参考:Memory_Barriers_a_Hardware_View_for_Software_Hackers
std::memory_order_relaxed(宽松内存序)介绍
关于std::memory_order_relaxed具备如下几个功能:
- 作用于原子变量
- 不具有synchronizes-with关系
- 对于同一个原子变量,在同一个线程中具有happens-before关系(言外之意,不同的原子变量不具有happens-before关系,可以乱序执行)
- 由3可知,在一个线程中,如果某个表达式已经看到原子变量的某个值a,则该表达式的后续表达式只能看到a或者比a更新的值
示例代码
如下代码摘抄至《C++ Concurrency in Action》
#include #include #include std::atomic x, y;std::atomic z;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 /* code */ } if (x.load(std::memory_order_relaxed)) { //4 ++z; } }int main(int argc, char* argv[]) { x = false; y = false; z = 0; std::thread t1(write_x_then_y); std::thread t2(read_y_then_x); t1.join(); t2.join(); assert(z.load() != 0); // 5 return 0;}
上述代码,在x86架构的机器上跑,永远不会触发 5,但是若是ARM架构,则可能触发5。
关于该代码的理解,我们可以结合上述CPU架构小节来理解,假设线程t1运行在CPU1,线程t2运行在CPU3,std::memory_order_relaxed在此处可以理解为仅仅保持原子性,没有其他的作用。因此线程1虽然更新x,y为true,但由于无法保证 两者都同时对其他CPU可见(每个CPU可能在任何时刻将其store buffer中的值写入cache或者memory,此时才有机会被其他CPU看见)。
因此上述可能存在如下执行顺序:
- 标记1执行,x为true
- 标记2执行,y为true
- CPU1将y写入cache或者memory,CPU3可以看见改值
- 标记3执行,y为true
- 标记4执行,cache中的x为false,z为0
- 标记5执行,触发断言
当然你也可以用以下执行顺序理解上述代码(由于std::memory_order_relaxed在不同变量间不具有happens-before关系,因此,标记2可以在标记1之前执行), 故也可能存在如下执行顺序:
- 标记2执行,y为true
- 标记3执行,y为true
- 标记4执行,x为false,z为0
- 标记1执行,x为true
- 标记5执行,触发断言
针对该种执行顺序,书中给出了如下示意图,相信目前你也能理解该图含义
再来看一段更复杂的例子,其同样来源于《C++ Concurrency in Action》
#include #include #include #include std::atomic x(0), y(0), z(0);std::atomic go;unsigned const int loop_num = 10;struct read_values { int x, y, z;}// 定义五个read_values数组read_values values1[loop_num];read_values values2[loop_num];read_values values3[loop_num];read_values values4[loop_num];read_values values5[loop_num];void increment(std::atomic* var_to_inc, read_values* values) { // 利用go标记启动线程,以保证所有线程能同时启动 while (!go) { // 让出CPU给其他线程 std::this_thread::yield(); } // 针对某个原子变量,在每次循环时将其赋值为i+1,并在下次load时读取 for (unsigned i = 0; i store(i+1, std::memory_order_relaxed); std::this_thread::yield(); } return;}void read_vals(read_values* values) { while (!go) { std::this_thread::yield(); } for (unsigned i = 0; i < loop_num; ++i) { values[i].x = x.load(std::memory_order_relaxed); values[i].y = y.load(std::memory_order_relaxed); values[i].z = z.load(std::memory_order_relaxed); std::this_thread::yield(); }}void print(read_values* v) { for (unsigned i = 0; i < loop_num; ++i) { if (i) { std::cout << ", "; } std::cout<<"("<<v[i].x<<","<<v[i].y<<","<<v[i].z<<")"; } std::cout<<std::endl;}int main(int argc, char* argv[]) { std::thread t1(increment,&x,values1); std::thread t2(increment,&y,values2); std::thread t3(increment,&z,values3); std::thread t4(read_vals,values4); std::thread t5(read_vals,values5); go=true; t5.join(); t4.join(); t3.join(); t2.join(); t1.join(); print(values1); print(values2); print(values3); print(values4); print(values5); return 0;}
上述代码一个可能结果如下:
(0,0,0),(1,0,0),(2,0,0),(3,0,0),(4,0,0),(5,7,0),(6,7,8),(7,9,8),(8,9,8),(9,9,10)(0,0,0),(0,1,0),(0,2,0),(1,3,5),(8,4,5),(8,5,5),(8,6,6),(8,7,9),(10,8,9),(10,9,10)(0,0,0),(0,0,1),(0,0,2),(0,0,3),(0,0,4),(0,0,5),(0,0,6),(0,0,7),(0,0,8),(0,0,9)(1,3,0),(2,3,0),(2,4,1),(3,6,4),(3,9,5),(5,10,6),(5,10,8),(5,10,10),(9,10,10),(10,10,10)(0,0,0),(0,0,0),(0,0,0),(6,3,7),(6,5,7),(7,7,7),(7,8,7),(8,8,7),(8,8,9),(8,8,9)
理解为什么会出现该结果依然需要结合CPU架构小节的内容,std::memory_order_relaxed内存序针对同一个原子变量,在同一个线程具有happens-before关系, 因此若在同一个线程中,先store一个值,则后续load必然会看到这个store的值,因此values1,values2,values3的输出中,x,y,z均是单调递增的。
同上述讲解一样,对于不同的线程,std::memory_order_relaxed内存序不保证读取值的同步,但若同一个线程已经读取到某个值a,则后续的load不能读取到比a更老的值。
因此便会出现上述结果!
开发者涨薪指南
48位大咖的思考法则、工作方式、逻辑体系