喵の窝

C++11 中的内存模型

背景

在c++11中增加了atomic原子库,库里面提供了基础数据类型的原子读、写、自增自减等操作,但是如果我们看API文档的话,会发这些操作除了必要的参数以外,还有一个类型是std::memory_order(内存模型)的参数,而这个参数的默认值是std::memory_order_seq_cst。这个参数究竟有什么作用呢,我们详细说明一下。

内存模型

c++11中的内存模型是用来同步多个CPU(或者CPU核心)之间的数据的。比如在一个CPU(或者CPU核心)中改变了某个变量的值,其它CPU(或者CPU核心)能否看到改变后的值,或者什么时候可以看到改变后的值就是通过内存模型来控制的。c++11提供了5种内存模型,分别是

  • memory_order_relaxed
  • memory_order_consume
  • memory_order_acquire
  • memory_order_release
  • memory_order_seq_cst

其中memory_order_acq_rel并不是一个内存模型,只是memory_order_acquire和memory_order_release的合并,在一些读取并且修改的操作中(比如cas操作)用到。
这5中内存模型中,有些用于写值(store),有些用于读值(load),有些需要配对使用,有些两者都能用。下面这张表是所有的用法

load store
memory_order_seq_cst memory_order_seq_cst
memory_order_acquire memory_order_release
memory_order_consume memory_order_release
memory_order_relaxed memory_order_relaxed

memory_order_seq_cst/memory_order_seq_cst

seq_cst是sequentially consistent(顺序一致)的缩写,也是默认的内存模型。这种内存模型会在所有CPU(或者核心)中,保持严格的代码线性执行顺序。如下面代码(x为普通变量,y,z为原子变量)

1
2
3
4
5
-Thread 1-       -Thread 2-
x = 1 if (z.load() == 3) {
y.store(2); assert(x == 1)
z.store(3); assert(y.load() == 2)
}

如果线程2中读到的z值为3,那么,在线程1中,由于x = 1和y.sotre发生在z.store之前,所以在线程2中也保持了这种顺序。于是线程2中的两个assert是不可能失败的。这种模型具有最强的数据一致性,因此性能也最差。

memory_order_acquire/memory_order_release

这种模型比memory_order_seq_cst稍微宽松一些,它移除了不相关的原子变量的顺序一致性。还是上面那段代码。如果我们把内存模型改为acquire和release,比如

1
2
3
4
5
6
7
8
9
10
-Thread 1-
x = 1
y.store(2, memory_order_release);
z.store(3, memory_order_release);

-Thread 2-
if (z.load(memory_order_acquire) == 3) {
assert(x == 1)
assert(y.load(memory_order_acquire) == 2)
}

如果线程2中的z读到了3,由于原子变量中的y使用的是memory_order_acquire,因此线程1中的 ‘y先store,z再store’ 的顺序并不能保持在线程2中,因此assert(y.load(memory_order_acquire) == 2)可能(但不是一定)失败。而由于x不是原子变量,因此x = 1发生在z.store之前是在线程2中也是可以保证的。因此assert(x == 1)不会失败。
但是,如果把线程1中z.store替换为z.store(y.load(memory_order_acquire) + 1, memory_order_release)。在这种情况下,由于z依赖y,所以在线程2中,y.store发生在z.store的顺序还是可以保证的。

memory_order_consume/memory_order_release

这种内存模型又比memory_order_seq_cst更加宽松,它不仅移除了不相关的原子变量的顺序一致性,还移除了不相关普通变量的一致性。如果把上面代码中的memory_order_acquire改为memory_order_consume,那么线程2中的两个assert都有可能失败。同样的,如果某个原子变量依赖其它的变量(原子或者非原子)的话,那么这两个变量间的顺序一致性是可以保证的。

memory_order_relaxed/memory_order_relaxed

这种模型最宽松,他并不对任何顺序做保证,仅仅保证操作的原子性。

最后,千万不要混用内存模型,比如load用memory_order_seq_cst,store用memory_order_relaxed。不作死就不会死。

参考资料

cppreference memory order
Memory model synchronization modes