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

简介: 理解内存序,指令重排与内存模型

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 <atomic>
#include <thread>
#include <assert.h>
#include <iostream>

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

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

4.2 std::memory_order_release

memory_order_release(释放操作),在写入某原子对象时,

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

测试代码如下:

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 多线程同步:当多个线程需要共享一个变量,并且需要确保每个线程对该变量的读操作都能够正确地获取最新值时,可以使用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 <atomic>
#include <thread>

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();
}

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  等都不指定内存序的时候

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

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

相关文章
|
缓存
指令重排序的探讨
指令重排序是现代处理器为了提高指令级并行性和性能而进行的一种优化技术。在高并发场景下,指令重排序可能会引发一些问题,本文将详细介绍指令重排序的概念、原因、影响以及如何解决这些问题。
180 0
|
3月前
|
存储 程序员 编译器
堆和栈内存的区别是什么
【8月更文挑战第23天】堆和栈内存的区别是什么
198 4
|
6月前
|
程序员
内存操作数及寻址方式
内存操作数及寻址方式
81 0
|
6月前
|
Java 编译器 程序员
【面试问题】什么是指令重排?
【1月更文挑战第27天】【面试问题】什么是指令重排?
|
编译器
什么是指令重排序?
什么是指令重排序?
什么是指令重排序?
|
安全 Java
JVM 三色标记法与读写屏障
JVM 三色标记法与读写屏障
JVM 三色标记法与读写屏障
|
缓存 编译器 C语言
指令重排序与内存屏障
指令重排序与内存屏障
273 0
指令重排序与内存屏障
|
SQL 缓存 安全
指令重排&happens-before 原则 & 内存屏障
指令重排&happens-before 原则 & 内存屏障
254 0
指令重排&happens-before 原则 & 内存屏障
|
安全 Java
JVM 三色标记法与读写屏障(上)
GC 垃圾回收器其主要的目的是为了实现内存的回收,在这个过程中主要的两个步骤就是:内存标记,内存回收。
210 0
JVM 三色标记法与读写屏障(上)