原子操作内存序

[TOC]

参考

1. C++11多线程-内存模型
2. c++并发编程1.内存序
3. 浅谈Memory Reordering
4. C++11中的内存模型下篇 - C++11支持的几种内存模型
5. C++11中的内存模型上篇 - 内存模型基础

前言

有三种情况,可能导致乱序执行:编译器优化、CPU乱序、缓存不一致。进而导致多线程情况下出现问题。[1,3,4]

c++11引入了atomic类型之后,大大方便了原子变量的使用,但是原子变量的内存序有好几种,这又引入了让人难以理解的内容。
内存序分为三类六种

  • relaxed(松弛的内存序)
  • sequential_consistency(内存一致序)
  • acquire-release(获取-释放一致性)

relaxed

//test.cpp
#include 
#include 
#include 

std::atomic x{false},y{false};
std::atomic z{0};

void write_x_then_y() {
    x.store(true,std::memory_order_relaxed);   //1
    y.store(true,std::memory_order_relaxed);   //2
}

void read_y_then_x() {
    while(!y.load(std::memory_order_relaxed));  //3
    if(x.load(std::memory_order_relaxed))     //4
        ++z;
}

int main() {
    std::thread b(read_y_then_x);
    std::thread a(write_x_then_y);
    a.join();
    b.join();
    if (z.load() != 0) return 0; else return 1;
}
# test.sh
#!/bin/bash
for ((i=0;i<1;)); do
    ./a.out
    if [ "$?" == "1" ];then
        break
    fi
done

g++ -std=c++17 -pthread -O2 test.cpp编译以上代码,time sh test.sh执行代码。

如果出现 2 -> 3 -> 4 -> 1这样的执行次序,那么就会出现z == 0这种错误情况。

注,不过我跑了一晚上,并没有复现这个结果

那么relaxed用于何处呢?对于计数这种场景,就可以使用relaxed来最大化性能。

#include 
#include 
#include 
#include 

std::atomic count{0};
void f() {
    for (int n = 0; n < 1000; ++n) {
        count.fetch_add(1, std::memory_order_relaxed);
    }
}
int main() {
    std::thread threads[10];
    for (std::thread &thr: threads) {
        thr = std::thread(f);
    }
    for (auto &thr : v) {
        thr.join();
    }
    assert(cnt == 10000); // 永远不会失败
    return 0;
}

release-acquire

针对relaxed的例子,如果改成如下的代码就可以避免z == 0这种错误情况。

#include 
#include 
#include 

std::atomic x{false},y{false};
std::atomic z{0};

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
        ++z;
}

int main() {
    std::thread b(read_y_then_x);
    std::thread a(write_x_then_y);
    a.join();
    b.join();
    if (z.load() != 0) return 0; else return 1;
}

他会保证1发生在2前,4发生在3后,同时3一定发生在2后,那么z == 0不会发生。

如下图分析

image.png
  • 初始条件为x = y = false。
  • 在write_x_then_y线程中,先执行对x的写操作,再执行对y的写操作,由于两者在同一个线程中,所以即便针对x的修改操作使用relaxed模型,修改x也一定在修改y之前执行。
  • 在write_x_then_y线程中,对y的load操作使用了acquire模型,而在线程write_x_then_y中针对变量y的读操作使用release模型,因此保证了是先执行write_x_then_y函数才到read_y_then_x的针对变量y的load操作。
  • 因此最终的执行顺序如上图所示,此时不可能出现z=0的情况。

从以上的分析可以看出,针对同一个变量的release-acquire操作,更多时候扮演了一种“线程间使用某一变量的同步”作用,由于有了这个语义的保证,做到了线程间操作的先后顺序保证(inter-thread happens-before)。

可以简单记作release为写不后,acquire为读不前。[2]

release-consume

官方不推荐,此处不进行详细描述。简单说,release-acquire会把不相关的变量存取都进行保序,release-consume只会对有依赖的变量保序,进而提高效率,同时也使代码更容易引入bug。

sequential consistency

这是最严格的级别,也是性能最差的级别,同时也是默认的级别。
如下列:

#include 
#include 
#include 
 
std::atomic x = {false};
std::atomic y = {false};
std::atomic z = {0};
 
void write_x() {
    x.store(true, std::memory_order_seq_cst);  // 1
}
 
void write_y() {
    y.store(true, std::memory_order_seq_cst);  // 2
}
 
void read_x_then_y() {
    while (!x.load(std::memory_order_seq_cst));  // 3
    if (y.load(std::memory_order_seq_cst)) {  // 4
        ++z;
    }
}
 
void read_y_then_x() {
    while (!y.load(std::memory_order_seq_cst));  // 5
    if (x.load(std::memory_order_seq_cst)) {   // 6
        ++z;
    }
}
 
int main() {
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y); // thread c
    std::thread d(read_y_then_x); // thread d
    a.join(); b.join(); c.join(); d.join();
    // failed to assert without memory_order_seq_cst
    assert(z.load() != 0);
}

如果使用release-acquire,那么线程c可能看到的是2 -> 1这个执行顺序,但是线程d可能看到的是1->2这个执行顺序,进而导致z == 0

如下图分析:


image.png
  • 初始条件为x = y = false。
  • 由于在read_x_and_y线程中,对x的load操作使用了acquire模型,因此保证了是先执行write_x函数才到这一步的;同理先执行write_y才到read_y_and_x中针对y的load操作。
  • 然而即便如此,也可能出现在read_x_then_y中针对y的load操作在y的store操作之前完成,因为y.store操作与此之间没有先后顺序关系;同理也不能保证x一定读到true值,因此到程序结束是就出现了z = 0的情况。

从上面的分析可以看到,即便在这里使用了release-acquire模型,仍然没有保证z==0,其原因在于:最开始针对x、y两个变量的写操作是分别在write_x和write_y线程中进行的,不能保证两者执行的顺序导致。

你可能感兴趣的:(原子操作内存序)