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

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

@TOC

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 配对使用;
这里意味着执行顺序 可能是
正常 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 多线程同步:当多个线程需要共享一个变量,并且需要确保每个线程对该变量的读操作都能够正确地获取最新值时,可以使用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服务期高级架构系统教程学习:链接

相关文章
|
6天前
|
存储
【汇编】数据在哪里?有多长、div指令实现除法、dup设置内存空间
【汇编】数据在哪里?有多长、div指令实现除法、dup设置内存空间
|
6天前
|
存储 程序员 数据处理
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
161 1
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
|
9月前
|
缓存 安全 Java
volatile底层的实现原理:volatile关键字的作用、内存模型、JMM规范和CPU指令
volatile底层的实现原理:volatile关键字的作用、内存模型、JMM规范和CPU指令
125 0
|
10月前
持久内存指令(PMDK)简介
持久内存指令(PMDK)简介
226 0
|
10月前
|
存储 安全
4.3 x64dbg 搜索内存可利用指令
发现漏洞的第一步则是需要寻找到可利用的反汇编指令片段,在某些时候远程缓冲区溢出需要通过类似于`jmp esp`等特定的反汇编指令实现跳转功能,并以此来执行布置好的`ShellCode`恶意代码片段,`LyScript`插件则可以很好的完成对当前进程内存中特定函数的检索工作。在远程缓冲区溢出攻击中,攻击者也可以利用汇编指令`jmp esp`来实现对攻击代码的执行。该指令允许攻击者跳转到堆栈中的任意位置,并从那里执行恶意代码。
122 0
|
12月前
|
存储 缓存 索引
通过地址和索引实现数组、CPU指令执行过程、内存概述及内存物理结构
通过地址和索引实现数组、CPU指令执行过程、内存概述及内存物理结构
68 0
|
前端开发 rax
实验一:查看CPU和内存,用机器指令和汇编指令编程
实验一:查看CPU和内存,用机器指令和汇编指令编程
159 0
|
存储 缓存 数据库
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽
94 0
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽
|
安全 Java 编译器
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
|
存储 缓存 Java
关于缓存一致性协议、MESI、StoreBuffer、InvalidateQueue、内存屏障、Lock指令和JMM的那点事
关于缓存一致性协议、MESI、StoreBuffer、InvalidateQueue、内存屏障、Lock指令和JMM的那点事
292 0