美文网首页
原子操作内存序

原子操作内存序

作者: GOGOYAO | 来源:发表于2020-05-05 12:07 被阅读0次

    [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 <thread>
    #include <atomic>
    #include <assert.h>
    
    std::atomic<bool> x{false},y{false};
    std::atomic<int> 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这种错误情况。

    <font color=red>注,不过我跑了一晚上,并没有复现这个结果</font>

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

    #include <cassert>
    #include <vector>
    #include <thread>
    #include <atomic>
    
    std::atomic<int> 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 <thread>
    #include <atomic>
    #include <assert.h>
    
    std::atomic<bool> x{false},y{false};
    std::atomic<int> 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 <thread>
    #include <atomic>
    #include <cassert>
     
    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);  // 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线程中进行的,不能保证两者执行的顺序导致。

    相关文章

      网友评论

          本文标题:原子操作内存序

          本文链接:https://www.haomeiwen.com/subject/scixqhtx.html