接上篇 c++标准库:并发(四) —— 条件变量 std::condition_variable
基本例子
在c++标准库:并发(三) —— 锁 mutex 和 lock 中有提到并发执行时可能使程序挂掉的几种情况。在(三)和(四)中通过锁来解决了同步执行的问题。
可以发现,引起致命问题的三种情况全部是和内存操作相关的,而加锁的代价可能导致频繁的用户态/内核态切换,相对还是比较高。c++提供了原子变量来提供一些原子操作和对内存模型更加精准的控制。
首先来看(三)中关于unique_lock的例子,我们可以用原子变量来替代加锁赋值、判断,得到更高的性能。
1 | std::atomic<bool> readyFlag(false); |
- atomic变量操作会提供原子特性,不会出现data race的情况
- store会保证其之前的语句,无论是否atomic,的内存操作都对其它线程可见
- load会保证其后的语句,无论是否atomic,的内存操作都对其它内存可见
- 不想用sleep,选择condition_variable时,还是需要加锁才能保护条件变量的消费
其原因是atomic所有的操作都默认使用 memory_order_seq_cst 顺序一致的内存次序,后面会说到。
一些高级接口
上面是《C++标准库》中列举的高级API,单独说明的有这几个:
- is_lock_free:用于判断是否是硬件能力支持的原子操作
- compare_exchange_strong cas操作(compare and swap)。理解为原子操作的updateByQuery。
- compare_exchange_weak 同上,弱化校验版,compare的步骤中可能出现假失败(即其实一样判断为了不一样),但是性能更好。
低级接口
低层操作允许很多原子操作传入第二个参数,以对内存序进行更加精细化的控制,从而在不需要完全全局同步原子操作的时候,允许编译器以性能更优的方式去操作内存。
低层操作概览
这些操作都支持设置memory_order来实现更精细化的内存序控制。
另外还提供了如 atomic_thread_fence
和atomic_signal_fence
来手动编写fence,安排内存访问策略,这里不深入研究了。
原子变量6种内存序
低阶API支持设置memory_order,而memory_order支持以下几种:
名称 | 规则 |
---|---|
memory_order_seq_cst | 默认的模式,也是最严格顺序同步的模式。所有线程中的该类型原子操作有个全局排序。原子操作前的内存操作不能重排到其后,后面的内存操作不能重排到其前。多线程间,其前的内存操作对其它线程可见。【注意】:这里的原子变量是所有,可以是不同原子变量,下面其它模式都要求是同一个原子变量。 |
memory_order_relaxed | 最宽松的模式,只保证原子变量的原子性,多线程操作时不出现data race,无任何内存操作顺序上的保证。可用于不在乎顺序只在乎原子性的场景,如全局计数。 |
memory_order_release | release操作前的内存操作保证对其它线程可见。其前的内存操作不能重排到其后,但是其后的内存操作还是可能随便重排。 |
memory_order_acquire | 原子操作后的内存操作不能重排到其前,但是其前的内存操作还是可能随便重排。一般与memory_order_release搭配使用,在多线程之间保证acquire后的数据一定能访问到release之前的数据。 |
memory_order_consume | 是弱化版memory_order_acquire。acquire后的内存操作一定不能重排到其前,但是consume仅仅保证依赖该原子操作的内存操作不重排到其前,而对其它内存操作不做保证。 |
memory_order_acq_rel | 有点像语法糖,被这个标记的原子操作,同时具有release和acquire的特点。 |
说实话,文字版的描述着实有点抽象,还是我根据自己的理解整理的。 所以还是尽量来举个栗子吧。
伪代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//全局变量A
std::atomic<int> A(0);
///////线程1
MEM_OP_1;
int a = MEM_OP_2;
A.store(a, memory_order_x);//原子操作依赖MEM_OP_2操作,设置内存序为memory_order_x
MEM_OP_3;
MEM_OP_4;
///////线程2
MEM_OP_1;
MEM_OP_2;
int a = A.load(memory_order_y); //设置内存序为memory_order_y
int b = a + MEM_OP_3; //这一步依赖A的原子操作
MEM_OP_4;
上面例子中,MEM_OP_* 为任意一种内存操作,可能为普通内存操作,也可能为原子变量的内存操作,那么列举几种常见的情况。
(① 表格中memory_order_x代表线程1中对原子变量A操作设置的内存序,memory_order_y代表对线程2中的原子操作设置的内存序; ② xx-前/后
,代表线程中原子操作前后的内存操作;③ 注意在单线程中有依赖关系的操作,编译器不会给乱重排。)
memory_order_x | memory_order_y | 线程1-前 | 线程1-后 | 线程2-前 | 线程2-后 |
---|---|---|---|---|---|
memory_order_seq_cst(默认) | memory_order_seq_cst(默认) | 不能重排到原子后,不能重排到两个相邻原子操作之外 | 不能重排到原子前,不能重排到两个相邻原子操作之外 | 不能重排到原子后,不能重排到两个相邻原子操作之外 | 不能重排到原子前,不能重排到两个相邻原子操作之外 |
memory_order_release | memory_order_acquire | 不能重排到原子后 | 随意 | 随意 | 不能重排到原子前 |
memory_order_release | memory_order_consume | 不能重排到原子后 | 随意 | 随意 | MEM_OP_3不能重排到原子前,其它随意 |
memory_order_relaxed | memory_order_relaxed | MEM_OP_2不能重排到原子后,其它随意 | 随意 | 随意 | MEM_OP_3不能重排到原子前,其它随意 |
memory_order_acq_rel
代表同时拥有acquire
和release
的特性,这里就不再列举了。
本文链接:https://www.zoucz.com/blog/2021/06/14/c6243960-cd07-11eb-9fe7-534bbf9f369d/