在 cpp11 标准原子库中(std::atomic),大多数函数都接受一个参数:std::memory_order:
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;
enum class memory_order : /*unspecified*/ {
relaxed, consume, acquire, release, acq_rel, seq_cst
};
inline constexpr memory_order memory_order_relaxed = memory_order::relaxed;
inline constexpr memory_order memory_order_consume = memory_order::consume;
inline constexpr memory_order memory_order_acquire = memory_order::acquire;
inline constexpr memory_order memory_order_release = memory_order::release;
inline constexpr memory_order memory_order_acq_rel = memory_order::acq_rel;
inline constexpr memory_order memory_order_seq_cst = memory_order::seq_cst;
(C++20 起)
std::memory_order
指定内存访问,包括常规的非原子内存访问,如何围绕原子操作排序。在没有任何制约的多处理器系统上,多个线程同时读或写数个变量时,一个线程能观测到变量值更改的顺序不同于另一个线程写它们的顺序。其实,更改的顺序甚至能在多个读取线程间相异。一些类似的效果还能在单处理器系统上出现,因为内存模型允许编译器变换。
库中所有原子操作的默认行为提供序列一致顺序(见后述讨论)。该默认行为可能有损性能,不过可以给予库的原子操作额外的 std::memory_order
参数,以指定附加制约,在原子性外,编译器和处理器还必须强制该操作。
cpp原子库中所有原子操作的默认行为是序列一致的顺序(memory_order_seq_cst, 见后述讨论)。该默认行为可能有损性能,不过也可以传递给线程对原子操作额外的 std::memory_order 参数,以指定附加制约,在原子性外,编译器和处理器还必须强制该操作。
线程1 | 线程2 |
---|---|
① x = 1; | ③ y=2; |
② r1 = y; | ④ r2 = x |
假设编译器、CPU不对指令进行重排,也没有使用std::memory_order,且两个线程交织执行(假设以上四条语句都是原子操作)时共有4!/(2!*2!)=6种情况:
从上面看,最终r1和r2的最终结果共有3种,r1= r2 = 0的情况不可能出现。但是当四条语句不是原子操作时。有一种可能是CPU的指令预处理单元看到‘线程1’的两条语句没有依赖性(不管哪条语句先执行,在两条指令语句完成后都会得到一样的结果),会先执行r1=y再执行x=1,或者两条指令同时执行,这就是CPU的多发射和乱序执行。对于线程2也一样。这样一来就有可能出来r1= r2= 0的结果。执行顺序可能就是:②④①③
另外一种r1= r2 = 0的情况是:线程1和线程线程2分别在不同的CPU核上执行,大家都知道CPU中是有Cache和RAM的,简单的理解一下程序的执行都是从RAM->Cache->CPU,执行完后CPU->Cache->RAM,有一种可能是当Core1和Core2都将x,y更新到L1 Cache中,而还未来得及更新到RAM时,两个线程都已经执行完了第二条语句,此时也会出现r1= r2= 0。
另外一个就是,从编译器层面也一样,为了获取更高的性能,它也可能会对语句进行执行顺序上的优化(类似CPU乱序)。
因此,在编译器优化+CPU乱序+缓存不一致的多重组合下,情况不止以上三种。但不管哪一种,都不是我们希望看到的。那么如何避免呢?最简单的,也是首选的,方案当然是std::mutex。因为使用mutex对比atomic更容易分析程序出现的各种错误,对于mutex的错误,大多数都是漏了加锁,或者加锁次序错乱等问题,但是如果使用atomic就比较难排查问题。那std::atomic是不是就没啥用呢,当然不是,当程序对代码执行效率要求很高,std::mutex不满足时,就需要std::atomic上场了,因为std::atomic主要是为了提高性能而生的。
现在的编译器基本都支持指令重排,上述的现象也只是理想情况下的执行情况,因为顺序一致性代价太大不利于程序的优化。但是有时候你需要对编译器的优化行为做出一定的约束,才能保证你的程序行为和你预期的执行结果保持一致,那么这个约束就是内存模型。 如果想要写出高性能的多线程程序必须理解内存模型,编译器会给你的程序做静态优化,CPU 为了提升性能也有动态乱序执行的行为。总之,实际编程中程序不会完全按照你原始代码的顺序来执行,因此内存模型就是程序员、编译器、CPU 之间的契约。编程、编译、执行都会在遵守这个契约的情况下进行,在这样的规则之上各自做自己的优化,从而提升程序的性能。
多线程中要保证race-condition情况下的正确运行,mutex或者atomic限制是必要的,mutex我们前面的文章已经介绍过可以理解为就是synchronizes-with的关系,而atomic的限制有两种关系:synchronizes-with和happens-before的限制,确保在线程之间运行的顺序保证。
synchronizes-with关系是在原子类型的操作之间获得的关系。如果数据结构包含原子类型,并且对该数据结构的操作在内部执行适当的原子操作,则对数据结构的操作(例如锁定互斥体)可能提供这种关系,但基本上它仅来自对原子类型的操作。如果线程A存储一个值,而线程B读取该值,则线程A中存储和线程B中的加载之间存在同步关系。
从synchronizes-with的定义中我们可以看出,这种关系讲的就是线程之间的原子操作关系。
它指定哪些操作看到哪些其他操作的效果。如果一个线程上操作A在另一个线程上的操作B之前发生,则A在B之前发生。
我们举个例子来简单说明一下这几种关系,看下面的程序,write_x_then_y()与read_y_then_x()运行在不同的thread:
atd::atomic x(false);
atd::atomic y(false);
void write_x_then_y()
{
x.store(true,std::memory_order_relaxed); // 1
y.store(true,std::memory_order_release); // 2
}
void read_y_then_x()
{
while(!y.load(std::memory_order_acquire)); // 3
if(x.load(std::memory_order_relaxed)){ // 4
// do something
}
}
复制代码
上面的例子中,x原子变量采用的是relaxed order, y原子变量采用的是acquire-release order
内存的顺序描述了计算机CPU获取内存的顺序,内存的排序可能静态也可能动态的发生:
静态内存排序是为了提高代码的利用率和性能,编译器对代码进行了重新排序;同样为了优化性能CPU也会进行对指令进行重新排序、延缓执行、各种缓存等等,以便达到更好的执行效果。虽然经过排序确实会导致很多执行顺序和源码中不一致,但是你没有必要为这些事情感到棘手足无措。任何的内存排序都不会违背代码本身所要表达的意义,并且在单线程的情况下通常不会有任何的问题。 但是在多线程场景中,无锁的数据结构设计中,指令的乱序执行会造成无法预测的行为。所以我们通常引入内存栅栏这一概念来解决可能存在的并发问题。内存栅栏是一个令 CPU 或编译器在内存操作上限制内存操作顺序的指令,通常意味着在 barrier 之前的指令一定在 barrier 之后的指令之前执行。 在 C11/cpp11 中,引入了六种不同的 memory order,可以让程序员在并发编程中根据自己需求尽可能降低同步的粒度,以获得更好的程序性能。这六种 order 分别是:
typedef enum memory_order {
memory_order_relaxed, // 无同步或顺序限制,只保证当前操作原子性
memory_order_consume, // 标记读操作,依赖于该值的读写不能重排到此操作前
memory_order_acquire, // 标记读操作,之后的读写不能重排到此操作前
memory_order_release, // 标记写操作,之前的读写不能重排到此操作后
memory_order_acq_rel, // 仅标记读改写操作,读操作相当于 acquire,写操作相当于 release
memory_order_seq_cst // sequential consistency:顺序一致性,不允许重排,所有原子操作的默认选项
} memory_order;
memory_order_relaxed 宽松操作:没有同步或顺序制约,仅对此操作要求原子性(见下方宽松顺序)。
memory_order_consume 有此内存顺序的加载操作,在其影响的内存位置进行消费操作:当前线程中依赖于当前加载的该值的读或写不能被重排到此加载前。其他释放同一原子变量的线程的对数据依赖变量的写入,为当前线程所可见。在大多数平台上,这只影响到编译器优化(见下方释放消费顺序)。
memory_order_acquire 有此内存顺序的加载操作,在其影响的内存位置进行获得操作:当前线程中读或写不能被重排到此加载前。其他释放同一原子变量的线程的所有写入,能为当前线程所见(见下方释放获得顺序)。
memory_order_release 有此内存顺序的存储操作进行释放操作:当前线程中的读或写不能被重排到此存储后。当前线程的所有写入,可见于获得该同一原子变量的其他线程(见下方释放获得顺序),并且对该原子变量的带依赖写入变得对于其他消费同一原子对象的线程可见(见下方释放消费顺序)。
memory_order_acq_rel 带此内存顺序的读修改写操作既是获得操作又是释放操作。当前线程的读或写内存不能被重排到此存储前或后。所有释放同一原子变量的线程的写入可见于修改之前,而且修改可见于其他获得同一原子变量的线程。
memory_order_seq_cst 有此内存顺序的加载操作进行获得操作,存储操作进行释放操作,而读修改写操作进行获得操作和释放操作,再加上存在一个单独全序,其中所有线程以同一顺序观测到所有修改(见下方序列一致顺序)。
memory_order_relaxed |
宽松操作:没有同步或顺序约束,仅对此操作要求原子性(见下方宽松顺序)。 |
memory_order_consume |
有此内存顺序的加载操作,在其影响的内存位置进行消费操作:当前线程中依赖于当前值的读或写不能被重排到此加载前。其他释放同一原子变量的线程的对数据依赖变量的写入,为当前线程所可见。在大多数平台上,这只影响到编译器优化(见下方释放消费顺序)。 |
memory_order_acquire |
有此内存顺序的加载操作,在其影响的内存位置进行获得操作:当前线程中读或写不能被重排到此加载前。其他释放同一原子变量的线程的所有写入,能为当前线程所见(见下方释放获得顺序)。 |
memory_order_release |
有此内存顺序的存储操作进行释放操作:当前线程中的读或写不能被重排到此存储后。当前线程的所有写入,可见于获得该同一原子变量的其他线程(见下方释放获得顺序),并且对该原子变量的带依赖写入变得对于其他消费同一原子对象的线程可见(见下方释放消费顺序)。 |
memory_order_acq_rel |
带此内存顺序的读修改写操作既是获得操作又是释放操作。当前线程的读或写内存不能被重排到此存储前或后。所有释放同一原子变量的线程的写入可见于修改之前,而且修改可见于其他获得同一原子变量的线程。 |
memory_order_seq_cst |
有此内存顺序的加载操作进行获得操作,存储操作进行释放操作,而读修改写操作进行获得操作和释放操作,再加上存在一个单独全序,其中所有线程以同一顺序观测到所有修改(见下方序列一致顺序)。 |
除非为操作指定一个特定的序列,否则原子类型操作的内存序列默认都是memory_order_seq_cst。上面的六个类型可以统分为三大内存模型:
Relaxed ordering
#include
#include
#include
std::atomic x = 0;
std::atomic y = 0;
void f() {
int i = y.load(std::memory_order_relaxed); // 1 happen before 2
x.store(i, std::memory_order_relaxed); // 2
}
void g() {
int j = x.load(std::memory_order_relaxed); // 3
y.store(42, std::memory_order_relaxed); // 4
}
int main() {
std::thread t1(f);
std::thread t2(g);
t1.join();
t2.join();
std::cout << x << " - " << y << std::endl;
// 可能执行顺序为 4123,结果 x == 42, y == 42
// 可能执行顺序为 1234,结果 x == 0, y == 42
}
复制代码
#include
#include
#include
std::atomic x = 0;
std::atomic y = 0;
void f() {
std::cout << "1\n";
int i = y.load(std::memory_order_relaxed); // 1
std::cout << "2\n";
if (i == 42) {
x.store(i, std::memory_order_relaxed); // 2
}
}
void g() {
std::cout << "3\n";
int j = x.load(std::memory_order_relaxed); // 3
std::cout << "4\n";
if (j == 42) {
y.store(42, std::memory_order_relaxed); // 4
}
}
int main() {
std::thread t1(f);
std::thread t2(g);
t1.join();
t2.join();
std::cout << x << " - " << y << std::endl;
// 一般的顺序是1234
// 结果不允许为 x = 42, y = 42
// 因为要产生这个结果,1 依赖 4,4 依赖 3,3 依赖 2,2 依赖 1
}
复制代码
Relaxed ordering一般适用于只要求原子操作,不需要其它同步保障的情况。典型使用场景是自增计数器,比如 std::shared_ptr 的引用计数器,它只要求原子性,不要求顺序和同步
#include
#include
#include
#include
std::atomic x = 0;
void f() {
for (int i = 0; i < 1000; ++i) {
x.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector v;
for (int i = 0; i < 10; ++i) {
v.emplace_back(f);
}
for (auto& x : v) {
x.join();
}
std::cout << x; // 一定是输出 10000
}
复制代码
#include
#include
#include
std::atomic x;
int i;
void producer() {
int* p = new int(42);
i = 42;
x.store(p, std::memory_order_release);
}
void consumer() {
int* q;
while (!(q = x.load(std::memory_order_consume))) {
}
assert(*q == 42); // 一定不出错:*q 带有 x 的依赖
assert(i == 42); // 可能出错也可能不出错:i 不依赖于 x
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
复制代码
consume语义是一种弱的acquire,它只对关联变量进行约束,这个实际编程中基本不用,且在某些情况下会自动进化成acquire语义(比如当用consume语义修饰的load操作在if条件表达式中时)。另外,cpp17标准明确说明这个语义还未完善,建议直接使用acquire。
#include
#include
#include
std::atomic x;
int i;
void producer() {
int* p = new int(42); // 01
i = 42; // 02
x.store(p, std::memory_order_release); // 1 happens-before 2(由于 2 的循环)
}
void consumer() {
int* q;
while (!(q = x.load(std::memory_order_acquire))) { // 2
}
assert(*q == 42); // 一定不出错
assert(i == 42); // 一定不出错
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
复制代码
Release-Acquire ordering 并不表示 total ordering
#include
#include
std::atomic x = false;
std::atomic y = false;
std::atomic z = 0;
void write_x() {
x.store(true, std::memory_order_release); // 1 happens-before 3(由于 3 的循环)
}
void write_y() {
y.store(true, std::memory_order_release); // 2 happens-before 5(由于 5 的循环)
}
void read_x_then_y() {
while (!x.load(std::memory_order_acquire)) { // 3 happens-before 4,因为3使用了acquire
}
if (y.load(std::memory_order_acquire)) { // 4
++z;
}
}
void read_y_then_x() {
while (!y.load(std::memory_order_acquire)) { // 5 happens-before 6,因为5使用了acquire
}
if (x.load(std::memory_order_acquire)) { // 6
++z;
}
}
int main() {
std::thread t1(write_x);
std::thread t2(write_y);
std::thread t3(read_x_then_y);
std::thread t4(read_y_then_x);
t1.join();
t2.join();
t3.join();
t4.join();
// z可能为0:134执行则y为false,256执行则x为false,但1,2之间没有顺序关系
}
复制代码
为了使两个写操作有序,将其放到一个线程里
#include
#include
#include
std::atomic x = false;
std::atomic y = false;
std::atomic z = 0;
void write_x_then_y() {
x.store(true, std::memory_order_relaxed); // 1 happens-before 2,因为2使用了release
y.store(true, std::memory_order_release); // 2 happens-before 3(由于 3 的循环)
}
void read_y_then_x() {
while (!y.load(std::memory_order_acquire)) { // 3 happens-before 4,因为3使用了acquire
}
if (x.load(std::memory_order_relaxed)) { // 4
++z;
}
}
int main() {
std::thread t1(write_x_then_y);
std::thread t2(read_y_then_x);
t1.join();
t2.join();
assert(z.load() != 0); // 顺序一定为 1234,z一定不为 0
}
复制代码
利用 Release-Acquire ordering 可以传递同步,我们看下面的例子,最终实现1234这样的执行顺序,1 happens-before 2,3 happens-before 4,因为2:happens-before 3,所以1234顺序是必然的:
#include
#include
std::atomic x = false;
std::atomic y = false;
std::atomic v[2];
void f() {
// v[0]、v[1] 的设置没有先后顺序,但都 happens-before 1,因为1使用了release
v[0].store(1, std::memory_order_relaxed);
v[1].store(2, std::memory_order_relaxed);
x.store(true, std::memory_order_release); // 1 happens-before 2(由于 2 的循环)
}
void g() {
while (!x.load(std::memory_order_acquire)) { // 2:happens-before 3,因为2使用了acquire
}
y.store(true, std::memory_order_release); // 3 happens-before 4(由于 4 的循环)
}
void h() {
while (!y.load(std::memory_order_acquire)) { // 4 happens-before v[0]、v[1] 的读取
}
assert(v[0].load(std::memory_order_relaxed) == 1);
assert(v[1].load(std::memory_order_relaxed) == 2);
}
复制代码
使用读改写操作(memory_order_acq_rel)可以将上面的两个标记合并为一个,也能得到一样的效果,看下面的例子,也是必然安装123顺序执行:
#include
#include
std::atomic x = 0;
std::atomic v[2];
void f() {
v[0].store(1, std::memory_order_relaxed);
v[1].store(2, std::memory_order_relaxed);
x.store(1, std::memory_order_release); // 1 happens-before 2(由于 2 的循环)
}
void g() {
int i = 1;
// 如果x当前的值等于i则将x改写为2,返回true,如果不等则让i=x,并返回false
while (!x.compare_exchange_strong(i, 2, std::memory_order_acq_rel)) { // 2 happens-before 3(由于 3 的循环)
// x 为 1 时,将 x 替换为 2,返回 true
// x 为 0 时,将 i 替换为 x,返回 false
i = 1; // 返回 false 时,x 未被替换,i 被替换为 0,因此将 i 重新设为 1,再继续while
}
}
void h() {
while (x.load(std::memory_order_acquire) < 2) { // 3
}
assert(v[0].load(std::memory_order_relaxed) == 1);
assert(v[1].load(std::memory_order_relaxed) == 2);
}
复制代码
memory_order_seq_cst 是所有原子操作的默认选项,可以省略不写。对于标记为 memory_order_seq_cst 的操作,大概行为就是对每一个变量都进行上面所说的Release-Acquire操作,读操作相当于 memory_order_acquire,写操作相当于 memory_order_release,读改写操作相当于 memory_order_acq_rel,此外还附加一个单独的 total ordering,即所有线程对同一操作看到的顺序也是相同的。这是最简单直观的顺序,但由于要求全局的线程同步,因此也是开销最大的
#include
#include
#include
std::atomic x = false;
std::atomic y = false;
std::atomic z = 0;
// 要么 1 happens-before 2,要么 2 happens-before 1
void write_x() {
x.store(true); // 1 happens-before 3(由于 3 的循环)
}
void write_y() {
y.store(true); // 2 happens-before 5(由于 5 的循环)
}
void read_x_then_y() {
while (!x.load()) { // 3 happens-before 4
}
if (y.load()) { // 4 为 false 则 1 happens-before 2
++z;
}
}
void read_y_then_x() {
while (!y.load()) { // 5 happens-before 6
}
if (x.load()) ++z; // 6 如果返回false则一定是2 happens-before 1
}
int main() {
std::thread t1(write_x);
std::thread t2(write_y);
std::thread t3(read_x_then_y);
std::thread t4(read_y_then_x);
t1.join();
t2.join();
t3.join();
t4.join();
assert(z.load() != 0); // z 一定不为0
// z 可能为 1(134256) 或 2(123456),
// 1和2 之间必定存在 happens-before 关系,顺序要么12,要么21
}
复制代码
向大佬致敬
C++ 多线程12:内存模型(stdmemory_order)_c++ 多线程内存模型_uManBoy的博客-CSDN博客
std::memory_order - cppreference.com