# C++内存序
内存序是为了在做原子操作时,控制多线程间内存可见性的一种机制,从而有助于避免数据争用并确保正确同步。
之所以要对原子操作的内存序做限制还是在于编译器编译时和cpu
运行指令时会对指令做重排优化。这可能会导致意外的结果。关于指令乱序可以参考乱序执行与内存屏障 (opens new window),C++11中的内存模型上篇 - 内存模型基础 (opens new window)。
c++11
中引入的原子类型上的操作服从 6
种内存次序:
- memory_order_relaxed
- memory_order_consume
- memory_order_acquire
- memory_order_release
- memory_order_acq_rel
- memory_order_seq_cst
memory_order_seq_cst
是可选的最严格的内存次序,各种原子类型的所有操作都默认遵从该次序,除非我们特意为某项操作另行指定。
内存次序共有6
种,但它们只代表3
种模式:
- 先后一致次序,
memory_order_seq_cst
- 获取-释放次序,
memory_order_consume、memory_order_acquire、memory_order_release
和memory_order_acq_rel
- 宽松次序,
memory_order_relaxed
# 顺序一致性
顺序一致性是一种相对的内存序,它允许在原子操作之间插入任意数量的其他操作,只要这些操作遵循顺序一致性,那么原子操作的顺序就和它们在代码中的顺序一致。
顺序一致性Sequential Consistency
,简称SC
,所有处理器都只能看到一个单一的操作执行顺序,顺序一致性实际上是一种强一致性,可以想象成整个程序过程中由一个开关来选择执行的线程,这样才能同时保证顺序一致性。
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;
std::memory_order m = std::memory_order_seq_cst;
void write_x()
{
x.store(true, m);
}
void write_y()
{
y.store(true, m);
}
void read_x_then_y()
{
while(!x.load(m));
if(y.load(m))
++z;
}
void read_y_then_x()
{
while(!y.load(m));
if(x.load(m))
++z;
}
int main()
{
x=false;
y=false;
z=0;
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join();
b.join();
c.join();
d.join();
assert(z.load()!=0); ⇽--- ⑤
}
在上面的例子中,原子操作指定的是顺序一致性,所以z
的值一定不会为零,为什么会这说呢?
从read_x_then_y
函数这里来看,执行到if(y.load(m))
时,说明x.store
已经发生了,若此时y.load
返回false
说明y.store
操作还没有发生,这种情况下去执行线程d
的read_y_then_x
函数,到if(x.load(m))
时,x.load
一定是true
,因为线程c
中在顺序一致性的条件下已经确定了x.store
先于(happens-before
)y.store
,因此到线程d
中y.load
为true
时,x.load
一定为true
,如此,在顺序一致性的条件下,++z
一定会发生,assert(z!=0)
一定不会成立。
# 获取-释放
获取-释放内存序适用于共享数据被多个线程访问的情况。
一个线程对共享数据执行获取操作,另一个线程对共享数据执行释放操作。
获取操作必须发生在释放操作之前。
std::memory_order_acquire
,在指定此内存序后的读写操作都不能被重新排列到使用此内存序的操作之前,不过在此之前的指令可以被重排到该指令之后,有点像上锁的意思。
使用std::memory_order_acquire
内存时的执行顺序:
使用std::memory_order_acquire
内存时不会发生的执行顺序:
std::memory_order_release
,在指定此内存序之前的操作指令不能被重排到使用该内存序的指令之后,不过在此之后的指令可以被重排到该指令之前,有点像解锁的意思。
使用std::memory_order_release
内存时的执行顺序:
还以上面代码例子分析,不过store
操作的内存序都改成std::memory_order_release
,load
操作的内存序都改成std::memory_order_acquire
。
其对应的执行顺序为:
对于有些原子操作会同时使用到读写操作像compare_exchange_weak
,这种情况可以使用std::memory_order_acq_rel
。
# 宽松
宽松内存序适用于原子操作之间的操作没有任何关系的情况。std::memory_order_relaxed
是c++11
的六种内存序中最松散的内存序。不同线程之间针对该变量的访问操作先后顺序不能得到保证,即有可能乱序。
std::memory_order_relaxed 不保证内存操作的顺序,因此在一个线程中对原子变量的修改可能不会立即对其他线程可见。
如果程序依赖于某些顺序,而没有使用更严格的内存序来保证这个顺序,可能会出现数据竞争的问题。
编译器和处理器可能会重排序使用 std::memory_order_relaxed
的操作,这可能会导致难以预料和难以调试的行为。
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true,std::memory_order_relaxed);
y.store(true,std::memory_order_relaxed);
}
void read_y_then_x()
{
while(!y.load(std::memory_order_relaxed));
if(x.load(std::memory_order_relaxed))
++z;
}
int main()
{
x=false;
y=false;
z=0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load()!=0);
}
上面的代码由于使用了std::memory_order_relaxed
的内存序,因此x.store
和y.store
操作的顺序并无法保证是代码编写的顺序,所以assert(z!=0)
不一定成立。不过作者在现代x86 cpu
上跑了1000 000
次上面的程序一次也没有发生,在arm A77
上跑了1000 000
次也才触发了13
次。
# reference
1.https://www.codedump.info/post/20191214-cxx11-memory-model-1/ (opens new window)
2.https://www.codedump.info/post/20191214-cxx11-memory-model-2/ (opens new window)