• 理解内存序,指令重排与内存模型


    1 内存序的问题

    为了提高程序的效率,通常会对指令进行重排,也就是说代码在实际执行的时候与我们书写的顺序可能不同。
    通常有两种指令重排的方式:
    1 编译器优化重排
    2 cpu指令优化重排
    这种指令重排(重新排序),可能会引出一些非常难解的bug,因为从代码的表面看,不好分析出问题,所以必须要深入理解内存序。

    2 从原子操作里的内存序

    原子操作包含以下三个方面
    1 原子性: 锁ME状态
    2 同步性: 可见性 (不同线程看到的内存的值是一致的)
    3 顺序一致性 (代码执行顺序是不是跟我们写的代码顺序是一致的)
    其中的2 和 3 说的就是内存序。

    3 内存序规定了什么?

    假设核心0里有abcde 5个操作
    a,
    b,
    c, 只有c是原子操作
    d,
    e
    内存序规定了多个线程访问同一个内存地址时的语义:同步性 与 顺序一致性

    3.1 同步性

    某个线程对内存地址的更新何时能被其它线程看见 (核1能否看核0里面b更新的最新值)

    3.2 顺序一致性

    某个线程对内存地址访问附近可以做怎么样的优化 (d, e是否可以优化到c前面?a,b能否优化到c后面来?)

    4 内存模型,以C++为例

    4.1 std::memory_order_relaxed

    memory_order_relaxed (松散内存序): 只确保原子性,不具备同步性,顺序不一定,编译器,cpu都可以对ab、ef优化(改变顺序,重排)
    测试代码: ( 程序中v的值有可能不是1,只是理论上的,实际没测出来)

    #include 
    #include 
    #include 
    #include 
    
    std::atomic<bool> x,y;
    std::atomic<int> z;
    
    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()
    {
        for (int i=0; i < 10000; i++) {
            x=false;
            y=false;
            z=0;
            std::thread b(read_y_then_x);
            std::thread a(write_x_then_y);
            b.join();
            a.join();
            int v = z.load(std::memory_order_relaxed);
            if (v != 1)
                std::cout << v << std::endl;
        }
        return 0;
    }
    
    • 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

    应用场景:自增变量,不要求是最新值

    4.2 std::memory_order_release

    memory_order_release(释放操作),在写入某原子对象时,
    当前线程的任何前面的读写操作都不允许重排到这个操作的后面
    去(后面的可以优化到前面去),并且当前线程的所有内存写入都在对同一个原子对象进行获
    取的其他线程可见;通常与 memory_order_acquire 或
    memory_order_consume 配对使用;
    这里意味着执行顺序 可能是
    正常 abcde,
    也可以是 后面的优化到前面去了abdec

    测试代码如下:

    std::atomic<bool> x,y;
    std::atomic<int> z;
    
    void write_x_then_y()
    {
        x.store(true,std::memory_order_relaxed);  // 1 
        y.store(true,std::memory_order_release);  // 2   y = true x= true
        // 虽然x是松散的,但是y是释放操作,有同步性,所以它前面的x也顺带同步了,所以 read_y_then_x 中 x.load的值一定是true
    }
    
    void read_y_then_x()
    {
        while(!y.load(std::memory_order_acquire));  // 3 自旋,等待y被设置为true
        if(x.load(std::memory_order_relaxed))  // 4
            ++z;
    }
    
    int main()
    {
        x=false;
        y=false;
        z=0;
        std::thread a(write_x_then_y);
        std::thread b(read_y_then_x);
        a.join();
        b.join();
        std::cout << z.load(std::memory_order_relaxed) << std::endl;
        return 0;
    }
    
    • 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

    应用场景:
    1 多线程同步:当多个线程需要共享一个变量,并且需要确保每个线程对该变量的读操作都能够正确地获取最新值时,可以使用std::memory_order_acquire来确保同步。
    2 条件变量:条件变量通常用于多个线程之间进行协作和同步。在等待条件变量时,为了避免竞态条件和死锁问题,通常会使用std::memory_order_acquire来确保等待操作能够正确执行。

    4.3 std::memory_order_acquire

    memory_order_acquire (获得操作): 原子性,同步性,顺序性 与memory_order_release 相反
    应用场景:同4.2

    4.4 std::memory_order_consume

    不建议使用,已被弃用。

    4.5 std::memory_order_acq_rel

    获得释放操作,一个读‐修改‐写操作
    同时具有获得语义和释放语义,即它前后的任何读写操作都不允
    许重排,并且其他线程在对同一个原子对象释放之前的所有内存
    写入都在当前线程可见,当前线程的所有内存写入都在对同一个
    原子对象进行获取的其他线程可见;
    强调:仅保证当前线程执行该操作前和后对目标内存位置的读写操作能够同步到主存,并不保证其他线程能够看到相同的修改顺序。

    应用场景:适用于一些不需要全序列语义的多线程应用场景,它可以提供轻量级的内存序,从而避免不必要的性能开销。当程序中只需要进行acquire-release语义时可以选择使用std::memory_order_acq_rel以获得更好的性能。

    类似CAS: compare_exchange_strong compare_exchange_weak
    就compare_exchange_weak而言,看它的函数原型:
    compare_exchange_weak(T& expected, T val,
    memory_order success, memory_order failure),参数中就成功和失败都可以指定内存序。
    既涉及到读,也涉及到写。
    success 读写
    failure 仅写

    测试代码:

    #include 
    #include 
    
    std::atomic<int> data(0);
    bool flag = false;
    
    void writer() {
        data.store(42, std::memory_order_release); // 写入数据
        flag = true; // 设置标志位为 true
    }
    
    void reader() {
        while (!flag); // 等待标志位为 true
        int value = data.load(std::memory_order_acquire); // 读取数据
        std::cout << "value: " << value << std::endl;
    }
    
    int main() {
        std::thread t1(writer);
        std::thread t2(reader);
        t1.join();
        t2.join();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    4.6 std::memory_order_seq_cst

    memory_order_seq_cst:顺序一致性语义。
    代码简洁

    x.store(true)
    x.load()
    还有
    fetch_add
    fetch_sub
    fetch_add
    fetch_or
    fetch_xor  等都不指定内存序的时候
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    对于读操作相当于获得,对于写操作相当于释放,对于读‐修改‐写操作相当于获得释放,
    是所有原子操作的默认内存序,并且会对所有使用此模型的原子操作建立一个全局顺序,
    保证了多个原子变量的操作在所有线程里观察到的操作顺序相同,当然它是最慢的同步模型。

    强调:保证所有线程执行该操作时对目标内存位置所看到的顺序是一致的,即所有线程都能够看到相同的修改顺序
    文章参考与<零声教育>的C/C++linux服务期高级架构系统教程学习:链接

  • 相关阅读:
    网络安全工程师面试题整理
    Python基础
    Jenkins的介绍与相关配置
    Python中的文件操作和异常处理
    安装配置 hbase
    如何从报表控件FastReport .NET中连接到 PostgreSQL 数据库?
    点餐小程序实战教程08-购物车功能开发
    C++学习——string 详解(即C++字符串详解)
    SpringMVC:全局异常处理(动力)
    手把手改进yolo训练自己的数据(坑洼路面识别)
  • 原文地址:https://blog.csdn.net/qq43645149/article/details/131144611