彻底理解C++内存序

前言

c++官网的介绍: https://en.cppreference.com/w/cpp/atomic/memory_order

C++提供的原子变量api,基本都会提供一个入参选项,可以填内存序。很早之前我就学习了内存模型的相关概念,但是一直没有系统的学习和整理C++应该怎么用这些内存序。

首先直接给个总结

  1. 绝大多数程序逻辑只需要acq、rel和relaxed的内存序即可
  2. releaxed的内存用于对该变量读写值不关心的情况,比如读写顺序彻底乱排也不影响程序逻辑,比如只是用来计数统计结果
  3. seq_cst内存序的使用场景为,当存在多个线程和多个原子变量,并且程序逻辑强依赖于多个原子变量的读写时,需要全局存在一个原子变量读写顺序时,才使用该内存序。
  4. 其他情况都用acq或rel或两者结合的内存序即可。

C++内存序

C++内存模型

定义: 程序的可能执行顺序
六种内存序,保证了四种内存模型,其中consumer模型已经弃用

  1. 顺序一致性模型:我们写代码的顺序一样,内存操作不会发生乱序,C++默认模型
  2. Acquire-release模型: Acquire语义保证load之后的读写操作不会重排到load之前,Release语义保证store之前的读写操作不会重排到store之后,为X86架构默认的内存模型,即X86只支持Store-load之间的读写指令重排,是一种强模型

load 从内存中取值到寄存器, store将寄存器中的值送回内存

  1. Relaxed语义,几乎能进行一切指令重排(当然是无关变量之间比如 c= b, d= a就可以重排,X86就不会),Arm架构默认的内存模型。因此程序员可能需要手动通过内存模型或者C++的内存序保证执行的顺序。

一共定义了六个内存序,其中consume没见过,弃用。

1
2
3
4
5
6
7
8
9

typedef enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
} memory_order;

acquire与release内存序

以下例子即用了该内存序实现,不允许使用relaxed内存序,多个线程逻辑顺序由原子变量的读写过程控制。

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
#include <atomic>
#include <iostream>
#include <vector>

std::atomic_int indicator (0); // 初始值为零

int x, y = 0;

void thread_func1() {
x = y + 1;

// 通知thread_func2
indicator.store(1, // 写操作
std::memory_order_release);
}

void thread_func2() {
int ready = indicator.load( // 读操作
std::memory_order_acquire);

// 等待thread_func1
if (read > 0) {
y = 2;
}
}

以上x绝不可能等于3,即先store了,函数2通行后y=2,函数1再执行x = 2 + 1为3。如果使用releaxed内存序就有可能发生。

store操作确保了其前面的读写,即x=y+1无法排到其后面。而acquire操作确保了y的赋值一定在load后做。因为release不允许前面的指令排其后,acquire不允许其后面的指令排其前

seq_cst 内存序

之前介绍的acq_rel内存序基本能解决2个线程之间的同步需求了。只有比较极端的逻辑场景可能需要全局内存序,全局内存序的性能相比其他内存序是很差的。

参考stack_overflow的问题:https://stackoverflow.com/questions/12340773/how-do-memory-order-seq-cst-and-memory-order-acq-rel-differ

知乎也有相关回答:https://www.zhihu.com/question/8811713845/answer/75716283973?utm_psn=1874990550719004672

这里直接摘抄c++官方的例子,该例子必须要全局序才能实现逻辑。如果用acq_rel的话assert(z.load() != 0)可能失败,z可能等于0。因为存在情况,C线程认为x先store再ystore,而D线程相反。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <atomic>
#include <cassert>
#include <thread>

std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};

void write_x()
{
x.store(true, std::memory_order_seq_cst);
}

void write_y()
{
y.store(true, std::memory_order_seq_cst);
}

void read_x_then_y()
{
while (!x.load(std::memory_order_seq_cst))
;
if (y.load(std::memory_order_seq_cst))
++z;
}

void read_y_then_x()
{
while (!y.load(std::memory_order_seq_cst))
;
if (x.load(std::memory_order_seq_cst))
++z;
}

int main()
{
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); // will never happen
}