C++ Memory Order
C++11 Memory Order
为什么需要Memory Order
多线程下可能影响程序执行结果的行为:
- 原子操作(简单语句,C++也不保证是原子操作)
- 指令执行顺序(编译器可能优化代码使代码顺序发生变化,CPU可能会调整指令执行顺序)
- CPU可见性(再CPU cache的影响下,可能存在一个CPU执行了某个指令,不会立即被其他CPU所见)
例子1
int64_t i = 0;
// Thread-1
i = 100;
// Thread2
std::cout << i;
上面的代码输出可能是一个未定义的行为,由于不同CPU结构对指令的处理也不同,有的CPU再处理64为字节时需要两条指令,那这样i就不是原子操作,Thread-1和Thread-2的行为就变成了未定义.
例子2
std::atomic<int> x{0};
std::atomic<int> y{0};
// Thread-1:
x.store(100, std::memory_order_relaxed);
y.store(200, std::memory_order_relaxed);
// Thread-2:
while (y.load(std::memory_order_relaxed) != 200)
;
std::cout << x.load(std::memory_order_relaxed);
上面的例子,线程1对x和y分别写入100,200,并且x, y的操作属于原子操作,线程2等待y变成200之后,输出x的值,在这里x的值可能是0,由于再多线程下并没有保证指令指令顺序的一致性,所以上面的顺序是两个字:任意,那y.strore就可能执行再x.stroe的前面,导致x输出0.
例子3
int x = 0;
// Thread-1:
x = 100; // A
// Thread-2:
std::cout << x; // B
现在假设A在B之前执行,x的可能输出是多少?
x可能输出0,100,由于CPU可见性的问题,可能Thread-1中的A操作执行完,x的值还在高速缓存上,并没有更新到内存,之后Thread-2中的B去读取x的值就读取到了旧值.
以上三个例子分别对应的三种可能改变程序执行结果的行为(多线程下),C++11 memory order就是解决以上的问题,让C++用户写多线程程序时不需要考虑机器环境(这个时C++11 memory model功劳), 在C++11中可以提供了mutex,std::atomic...
C++11 std::atomic
c11 的顺序格式如下:
- Relaxed ordering
- Release_Acquire ordering
- Release_Consume ordering
- Sequentially-consistent ordering
分别对应了std::atomic中的顺序如下:
- std::memory_order_relaxed
- std::memory_order_acquire
- std::memory_order_release
- std::memory_order_consume
- std::memory_acq_rel
- std::memory_seq_cst
Relaxed ordering
std::atomic<int> x = 0;
std::atomic<int> y = 0;
// Thread-1:
r1 = y.load(memory_order_relaxed); // A
x.store(r1, memory_order_relaxed); // B
// Thread-2:
r2 = x.load(memory_order_relaxed); // C
y.stroe(99, memory_order_relaxed); // D
r1 == r2 == 99可能出现吗?
可能的, D->A->B->C的执行顺序就可能出现r1 == r2 == 99
解释: Relaxed ordering仅仅只保证atomic自带的方法是原子操作,除此之外,不提供任何跨线程的同步,由于不提供任何跨线程的同步,所以上面例子中的执行顺序就是两个字:任意,但是Relaxed ordering好处也非常明显,就是性能高.
Release-Acquire ordering
在这种模型下,load()采用memory_order_acquire, store()采用memory_order_release,提供了两个线程之间的主要功能.
1.限制指令重排
- 在store()之前的所有读写操作,不允许被移动到这个store()的后面.
- 在load()之后的所有读写操作,不允许被移动到这个load()前面.
2.线程之前可见
线程之间由于Cpu cache,导致线程之间不可见,Release-Acquire提供了线程之间的可见.
std::atomic<bool> ready{false};
int data = 0;
void producer() {
data = 10; // A
ready.store(true, std::memroy_order_release); // B
}
void consumer() {
while (!ready.load(std::memory_order_acquire)) // C
;
assert(data == 100); // D
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
}
上面例子中assert永远不会触发,根据上面的指令限制规则,A不可能移动到B的后面,D不可能移动到C的前面.
std::atomic<bool> f1 = false;
std::atomic<bool> f2 = false;
// thread1
f1.store(true, memroy_order_release); // A
if (!f2.load(memory_order_acquire)) { // B
// critical section
}
//thread2
f2.store(true, memroy_order_release); // C
if (!f1.load(memory_order_acquire)) { // D
// critical section
}
上面例子中两个if条件可能同时满足,并且同时进入临界区,根据上面的指令规则,B可以移动到A的前面,D可以移动到C的前面,这都不违法规则.
Release-Consume ordering
Release-Consume相比Release-Acquire,在功能上弱了一点,它仅仅将依赖于x(假设)的值进行同步.
Sequential consisten ordering
Release-Acquire就同步一个x,顺序一致会对所有的变量的所有原子操作都同步,这样,所有的原子操作就跟由一个线程执行似的.