最近看了极客时间——《现代C++实战三十讲》中的内存模型与Atomic一节,感觉对C++的内存模型理解还不是很清楚,看了后面的参考文献以及看了一些好的博客,算是基本了解了,根据参考文献整合一下。更多细节可以看看参考文献。
多个线程操作共享的变量,由于操作不是原子性的,很有可能会导致结果未定义。
int64_t i = 0; // global variable
Thread-1: Thread-2:
i = 100; std::cout << i;
对于上面的程序,线程1对一个int64类型的变量进行写操作,需要两个CPU指令,所以线程2可能会读到只执行1个CPU指令的中间状态,导致线程2输出未定义的变量。
Thread-1: Thread-2:
x = 1; if (y == 2) {
y = 2; x = 3;
y = 4;
}
对于上面的程序,x和y最后的结果可能会是1和4,这是因为编译器会根据上下文调整代码的执行顺序,使其最有利于处理器的架构,运行得更快。线程1中有可能先执行y的赋值,然后再执行x的赋值,执行到y的赋值,切换到线程2运行结束,再切换至线程1,就会导致1和4的结果。
当然最著名的乱序执行还是属于单例模式的double-check了。
指令乱序执行一节中的示例输出1和4其实还可能跟缓存一致性有关,现代处理器是多核的,每个核都有自己的缓存,对于y可能会先于x写入到内存当中,然后线程2执行结束,写入到内存,最后线程1的x再从缓存写入到内存。
更直观的是下面这个示例,线程1对x进行写操作,但可能还没来得及写入内存,线程2从内存中读入x打印,这也是缓存不一致所引起的。
int x = 0; // global variable
Thread-1: Thread-2:
x = 100; // A std::cout << x; // B
从上面的示例看出,多线程不约束会出很多问题,这里的解决方案是std::atomic。
C++11的内存模型共有6种,分四类。其中一致性的减弱会伴随着性能的增强。
atomic默认的模型是顺序一致性的,这种模型对程序的执行结果有两个要求:
这意味着将程序看做是一个简单的序列。如果对于一个原子变量的操作都是顺序一致的,那么多线程程序的行为就像是这些操作都以一种特定顺序被单线程程序执行。以单线程顺序执行的缺点就是效率低。
原子操作有三类:
还是上面的例子,这次把y改成atomic, Store(写操作)使用memory_order_release,条件判断的Load(读操作)使用memory_order_acquire。
Thread-1: Thread-2:
x = 1; if (y.load(memory_order_acquire) == 2) {
y.store(2, memory_order_release); x = 3;
y.store(4, memory_order_relaxed); //先不管
}
通过这样做,就可以得到我们想要的结果了。用下图示意一下,每一边的代码都不允许重排越过黄色区域,且如果 y 上的释放早于 y 上的获取的话,释放前对内存的修改都在另一个线程的获取操作后可见:
下面是获得和释放操作具体的作用:
还有一种读‐修改‐写操作,使用memory_order_acq_rel,含义如下:
在这种模型下,load()和store()都要带上memory_order_relaxed参数。Relaxed ordering 仅仅保证load()和store()是原子操作,除此之外,不提供任何跨线程的同步,乱序执行依然有。上面Acquire-Release的示例进入条件后,由于不再需要同步了,对循环内部进行重排序不会影响结果,性能还高。
该模型目前不鼓励使用,有兴趣可以看下面的参考链接。
【2】C++11中的内存模型下篇 – C++11支持的几种内存模型
【4】如何理解 C++11 的六种 memory order
【5】《现代C++实战三十讲》中的内存模型与Atomic