目录
引言
cpu架构
std::memory_order_relaxed(宽松内存序)介绍
示例代码
本人在某厂infra做C++相关开发,也会时常同C++并发编程打交道,因此决定将C++并发编程相关知识点记录成博客。本系列主要根据C++多线程并发实践这本书,分享相应的多线程编程的知识。
由于最近发现我辛苦写的文章被copy缺没有写上引用我这篇文章,导致我有点不舒服。所以我决定把这个专栏收费了。
就这样吧。我继续去出博客了,好久没更新了 希望在新的一年能更新一篇,提前祝大家2023年快乐。
欢迎关注我的公众号:松元漫话
本文是讲解C++内存序一列文章中的一部分,主要讲解宽松内存序的理解和使用。
本部分将从如下三方面讲解:
关于内存模型相关知识可参考C++多线程 内存序(顺序一致性)
一个粗略的现代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具备如下几个功能:
如下代码摘抄至《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看见)。
因此上述可能存在如下执行顺序:
当然你也可以用以下执行顺序理解上述代码(由于std::memory_order_relaxed在不同变量间不具有happens-before关系,因此,标记2可以在标记1之前执行), 故也可能存在如下执行顺序:
针对该种执行顺序,书中给出了如下示意图,相信目前你也能理解该图含义
再来看一段更复杂的例子,其同样来源于《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 < 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);
var_to_inc->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<<"("<
上述代码一个可能结果如下:
(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更老的值。
因此便会出现上述结果!
欢迎大家关注公众号互相交流(松元漫话)