c++11:std::memory_order(c++11 六种内存序)

指令重排

指令重排即通过改变指令的执行顺序,使程序在不改变结果的前提下,能够更快地运行。在现代计算机中,由于计算机硬件和操作系统的各种优化,指令重排已经成为了常见的优化手段。

然而,指令重排可能会导致程序出现错误或异常。这是因为,在多线程环境下,如果对共享数据进行读写,并且没有进行适当的同步,就有可能导致共享数据的值发生改变,从而影响到程序的正确性。

具体来说,如果一个线程在另一个线程修改共享变量之前读取该变量,那么它可能会看到一个过期的值,从而导致程序出现错误。此外,如果一个线程在另一个线程修改共享变量之后写入该变量,那么它也可能会覆盖掉另一个线程的修改结果,进而导致程序出现错误。

仅靠原子指令实现不了对资源的访问控制。这造成的原因是编译器和cpu实施了重排指令,导致读写顺序会发生变化,只要不存在依赖,代码中后面的指令可能会被放在前面,从而先执行它。cpu这么做是为了尽量塞满每个时钟周期,在单位时间内尽量执行更多的指令,从而提高吞吐率。

我们来看一个示例

我们先来看两个线程代码

// thread 1
// ready初始值为false
a.init();
ready = true;
// thread 2
if(ready)
{
 a.bar();
}

线程2在ready为true的时候会访问a,对线程1来说,如果按照正常的执行顺序,那么a先被初始化,然后在将ready赋为true。但对多核的机器而言,情况可能有所变化:

  • 线程1中的ready = true可能会被cpu或编译器重排到a.init()的前面,从而优先执行ready = true这条指令。在线程2中,a.bar()中的一些代码可能被重排到if(ready)之前。
  • 即使没有重排,ready和a的值也会独立地同步到线程2所在核心的cache,线程2仍然可能在看到ready为true时看到未初始化的a。

为了解决这个问题,我们一种解决方法是使用互斥元,还有一种就是cpu和编译器提供了memory fence,让用户可以声明访存指令的可见性关系,c++11总结为以下memory order:

头文件:#include<atomic>

typedef enum 

{

    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst

} memory_order;
(C++11 起)
(C++20 前)
enum class memory_order : /*unspecified*/ {

    relaxed, consume, acquire, release, acq_rel, seq_cst
};
inline constexpr memory_order memory_order_relaxed = memory_order::relaxed;
inline constexpr memory_order memory_order_consume = memory_order::consume;
inline constexpr memory_order memory_order_acquire = memory_order::acquire;
inline constexpr memory_order memory_order_release = memory_order::release;
inline constexpr memory_order memory_order_acq_rel = memory_order::acq_rel;

inline constexpr memory_order memory_order_seq_cst = memory_order::seq_cst;
(C++20 起)
memory order作用
memory_order_relaxed最弱的内存顺序,无fencing作用,没有任何顺序要求,cpu和编译器可以重排指令。原子操作可以以任意顺序执行,并且不保证与其他操作的顺序关系。这种顺序提供了最高的并发性,但可能导致读取操作看到过期的值或写入操作覆盖其他线程的写入结果。
memory_order_consume消费顺序要求在当前线程中,所有后续依赖于该原子操作的读操作都必须在原子操作完成后执行。这种顺序确保了读操作之间的依赖关系,并且对于写操作或其他线程中的读操作没有顺序要求。
memory_order_acquire获取顺序要求在当前线程中,所有后续的读操作都必须在原子操作完成后执行。这种顺序确保了当前线程对原子操作的读取操作不会被重新排序到原子操作之前,但对于写操作或其他线程中的读操作没有顺序要求。
memory_order_release在这条指令执行前的对内存的读写指令都执行完毕,这条语句之后的对内存的修改指令不能超越这条指令优先执行。这像一道栅栏。
memory_order_acq_rel是memory_order_acquire和memory_order_release的合并,这条语句前后的语句都不能被reorder。
memory_order_seq_cst比memory_order_acq_rel更加严格的顺序保证,memory_order_seq_cst执行完毕后,所有其它cpu都是确保可以看到之前修改的最新数据的。如果前面的几个memory order模式允许有缓冲存在的话,memory_order_seq_cst指令执行后则保证真正写入内存。一个普通的读就可以看到由memory_order_seq_cst修改的数据,而memory_order_acquire则需要由memory_order_release配合才能看到,否则什么时候一个普通的load能看到memory_order_release修改的数据是不保证的。

有了memory_order,我们可以这么改上面的例子:

// thread 1
// flag初始值为false
a.init();
flag.store(true, std::memory_order_release);  //前面的不能在我后面执行
// thread 2
if(flag.load(std::memory_order_acquire)) {   //后面的不能在我前面执行
 a.bar();
}

这样就保证了线程1和线程2的顺序性,比如线程2在看到flag==true时,能看到线程1 realse之前所有操作。 也就保证了代码符合我们的预期。

注意,memory fence(内存栅栏)不等于可见性,即使线程2恰好在线程1在把ready设置为true后读取了ready,也不意味着它能看到true,因为同步cache是有延时的。memory fence保证的是可见性的顺序:“假如我看到了a的最新值,那么我一定也得看到b的最新值”。

下面我们来看一些示例

1. memory_order_relaxed


#include <iostream>
#include <atomic>
#include <thread>
#include <unistd.h>

std::atomic<int> sharedValue(0);

// 线程1
void Thread1()
{
    sharedValue.store(5, std::memory_order_relaxed);
}

// 线程2
void Thread2()
{
    int value = sharedValue.load(std::memory_order_relaxed);
    // 可能输出0或5,由于松散顺序,读取操作的顺序不确定
    std::cout << "Value: " << value << std::endl;
}

int main()
{
    std::thread t1(Thread1);
    std::thread t2(Thread2);

    t1.join();
    t2.join();

    return 0;
}

 2. memory_order_consume

  • 10
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值