接上篇 c++标准库:并发(四) —— 条件变量 std::condition_variable

基本例子

c++标准库:并发(三) —— 锁 mutex 和 lock 中有提到并发执行时可能使程序挂掉的几种情况。在(三)和(四)中通过锁来解决了同步执行的问题。
可以发现,引起致命问题的三种情况全部是和内存操作相关的,而加锁的代价可能导致频繁的用户态/内核态切换,相对还是比较高。c++提供了原子变量来提供一些原子操作和对内存模型更加精准的控制。
首先来看(三)中关于unique_lock的例子,我们可以用原子变量来替代加锁赋值、判断,得到更高的性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
std::atomic<bool> readyFlag(false);
void prepareThread(){
//做某种准备工作,花费5秒钟
std::this_thread::sleep_for(std::chrono::seconds(5));
readyFlag.store(true);
}

void waitForPrepareThread(){
{
//死循环等待readyFlag变为true
while (!readyFlag.load())
{
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << " wait for prepare... " << std::endl;
}
}
std::cout << " prepare job done, run something! " << std::endl;
};

void TestAtomic(){
std::thread t1(prepareThread);
std::thread t2(waitForPrepareThread);
t1.join();
t2.join();
}
  • atomic变量操作会提供原子特性,不会出现data race的情况
  • store会保证其之前的语句,无论是否atomic,的内存操作都对其它线程可见
  • load会保证其后的语句,无论是否atomic,的内存操作都对其它内存可见
  • 不想用sleep,选择condition_variable时,还是需要加锁才能保护条件变量的消费

其原因是atomic所有的操作都默认使用 memory_order_seq_cst 顺序一致的内存次序,后面会说到。

一些高级接口

image.png
上面是《C++标准库》中列举的高级API,单独说明的有这几个:

  • is_lock_free:用于判断是否是硬件能力支持的原子操作
  • compare_exchange_strong cas操作(compare and swap)。理解为原子操作的updateByQuery。
  • compare_exchange_weak 同上,弱化校验版,compare的步骤中可能出现假失败(即其实一样判断为了不一样),但是性能更好。

低级接口

低层操作允许很多原子操作传入第二个参数,以对内存序进行更加精细化的控制,从而在不需要完全全局同步原子操作的时候,允许编译器以性能更优的方式去操作内存。

低层操作概览

image.png
这些操作都支持设置memory_order来实现更精细化的内存序控制。
另外还提供了如 atomic_thread_fenceatomic_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代表同时拥有acquirerelease的特性,这里就不再列举了。

☞ 参与评论