【一分钟学C++】std::memory_order
公众号: C++学习与探索 | 个人主页: rainInSunny | 个人专栏: Learn OpenGL In Qt
文章目录
- 写在前面
- 为什么需要Memory Order
- Memory Order
- Relaxed Order
- Release-Acquire Order
写在前面
使用std::memory_order
是用来限制编译器以及CPU对单线程当中的指令执行顺序进行重排的程度。这种限制,决定了以atom操作为基准点,对其之前的内存访问命令,以及之后的内存访问命令,能够在多大的范围内自由重排,从而形成了6种模式。这里我们主要讨论std::memory_order_relaxed
、std::memory_order_acquire
和std::memory_order_release
。 注意,std::memory_order
限制的是单线程中的CPU指令乱序,但一般用来解决多线程同步的问题。
为什么需要Memory Order
如果不使用任何同步机制(例如mutex或atomic),在多线程中读写同一个变量,程序的结果是难以预料的。简单来说,编译器以及CPU的一些行为,会影响到程序的执行结果:
- 即使是简单的语句,C++也不保证是原子操作。
- CPU可能会调整指令的执行顺序。
- 在CPU cache的影响下,一个CPU执行了某个指令,不会立即被其它CPU看见。
// 场景1:C++不保证线程2输出的是100,因为i = 100不是原子操作,可能存在中间态
int i = 0;
Thread_1:
i = 100;
Thread_2:
std::cout << i;
// 场景2:CPU可能会调整指令的执行顺序,这里假设所有操作都是原子操作,仍然可能输出0或者100
int x = 0;
int y = 0;
Thread_1:
x = 100;
y = 200;
Thread_2:
while (y != 200)
;
std::cout << x;
// 场景3:假设A先于B,但CPU cache的影响下,Thread_2不能保证立即看到A操作的结果,所以Thread_2可能输出0或100
int x = 0;
Thread_1:
x = 100; // A
Thread_2:
std::cout << x; // B
场景1,i = 100;
不是原子操作导致了结果不确定;场景2,CPU会在不影响当前线程执行逻辑情况下对指令执行顺序进行优化,如果Thread_1将y = 200
调整到x = 100
之前执行,那么可能输出0或者100;场景3,由于CPU缓存,可能导致Thread_2在输出的时候Thread_1中x
的值还不可见,导致可能输出0或者100。
typedef enum memory_order {
memory_order_relaxed, // relaxed
memory_order_consume, // consume
memory_order_acquire, // acquire
memory_order_release, // release
memory_order_acq_rel, // acquire/release
memory_order_seq_cst // sequentially consistent
} memory_order;
可以看出多线程读写变量需要同步机制,常见的有std::mutex
和std::atomic
,对比两者std::atomic
性能要更好一些。C++标准库提供了std::memory_order
和std::atomic
一起实现多线程之间同步,下面主要讲解std::memory_order_relaxed
、std::memory_order_acquire
和std::memory_order_release
。
Memory Order
Relaxed Order
std::atomic<int> x = 0;
std::atomic<int> y = 0;
Thread_1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
Thread_2:
r2 = x.load(memory_order_relaxed); // C
y.store(66, memory_order_relaxed); // D
执行完上面的程序,可能出现r1 == r2 == 66。理解这一点并不难,因为编译器允许调整C和D的执行顺序。如果程序的执行顺序是D -> A -> B -> C,那么就会出现r1 == r2 == 66。当只需要保证原子性,不需要其它同步操作时,选择使用std::memory_order_relaxed
。
Release-Acquire Order
在这种模型下,store()使用std::memory_order_release,而load()使用std::memory_order_acquire。这种模型有两种效果,第一种是可以限制CPU指令的重排。除此之外,还有另一种效果:假设 Thread_1中store()的那个值成功被Thread_2中load()到了,那么Thread_1在store()之前对内存的所有写入操作,此时对Thread_2来说都是可见的。
- 在store()之前的所有内存读写操作,不允许被移动到这个store()的后面。
- 在load()之后的所有内存读写操作,不允许被移动到这个load()的前面。
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<bool> ready{ false };
int data = 0;
void producer()
{
data = 66; // A
ready.store(true, std::memory_order_release); // B
}
void consumer()
{
while (!ready.load(std::memory_order_acquire)) // C
;
assert(data == 66); // D,never failed
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
由于在store()
之前的所有内存读写操作,不允许被移动到这个store()
的后面,所以线程t1中A操作一定在B操作之前就执行完毕了。线程t2中C操作跳出循环时意味着线程t1中B操作执行完毕,那么data此时肯定已经被赋值为66,所以assert永远不会失败。反之如果这里使用std::memory_order_relaxed
,那么t1线程中data的赋值可能被CPU调整到store()后面,那就可能导致assert失败。
关注公众号:C++学习与探索,有惊喜哦~