什么是内存屏障?,为什么需要内存屏障?

本文介绍了内存屏障的概念及其在多线程编程中的作用,防止编译器和处理器指令重排导致的逻辑错误。通过实例展示了在C++中如何使用内存屏障和不同内存顺序来确保并发安全,同时对比了锁和原子操作的差异及使用场景。此外,还详细列举了C++中六种不同的内存顺序及其应用场景。
摘要由CSDN通过智能技术生成

原文链接

1.什么是内存屏障?,为什么需要内存屏障?

首先,为了性能编译器和处理器都会对指令进行重排序

  • 什么是内存屏障:内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其后面的指令移到内存屏障指令之前。

  • 为什么需要内存屏障:编译器和处理器指令重排只能保证在单线程执行下逻辑正确,在多个线程同时读写多个变量的情况下,如果不对指令重排作出一定限制,代码的执行结果会根据指令重排后的顺序产生不同的结果。指令重排后的顺序每次执行时都可能不一样,显然我们希望我们的代码执行结果与代码顺序是逻辑一致的(可能不太准确),所以我们需要内存屏障。比如。

#define  DRINK_TEA        0
#define  PASS_CLASSROOM   1
#define  LEAVE_CLASSROOM  2

std::atomic<int> teacher_state(DRINK_TEA);
std::atomic<int> cat_state(DRINK_TEA);
std::atomic<int> your_state;

// thread1                                                                   
teacher_state.store(PASS_CLASSROOM, std::memory_order_relaxed); // A      
cat_state.store(PASS_CLASSROOM, std::memory_order_relaxed);  // B

// thread2
if (cat_state.load(std::memory_order_relaxed) == PASS_CLASSROOM) {
    your_state.store(LEAVE_CLASSROOM, std::memory_order_relaxed);
}

std::memory_order_relaxed:表面该原子操作不携带任何内存屏障。
上面例子做的事情为:

  • thread1:首先老师经过教室,然后猫经过教室。
  • thread2:检查猫是否经过了教室,如果猫经过了教室,那你就逃课。

在指令不重排的情况下,是ok的。你逃课之后不会被老师发现(不鼓励-_-)。但是如果指令重排把B排在了A前面。当你发现猫经过教室的时候,你离开教室,然后老师经过教室。这显然是不期望发生的。所以我们可以把A改为:

teacher_state.store(PASS_CLASSROOM, std::memory_order_acquire); // A   

std::memory_order_acquire:表明A指令之后的读写指令都不可以重排到A指令之前。即B不会发生在A,那么就可以安全的逃课了。

2. 锁与原子操作

2.1 锁的实现

自旋锁实现伪代码:

class SpinLock {
    atomic<int> lock_state_(0);

    lock() {
        // step1. 对原子变量进行CAS操作
        int expected = 0;
        while (!lock_state_.compare_exchange_strong(expected, 1, std::memory_order_acq_rel))
            ;
        // 获取锁成功

        // step2. 加上内存屏障指令,使得临界区的指令不会跑到临界区外面去
        __sync_synchronize(); // 内存屏障指令,与编译器实现,不同平台指令不同。
    }

    unlock() {
        // step1. 加上内存屏障指令,使得临界区的指令不会跑到临界区外面去
         __sync_synchronize();

         // step2.将原子变量设置为0
         lock_state_ = 0;
    }
};

互斥锁实现伪代码:

class SleepLock {
    bool _locked;
    SpinLock _spin_lock;

    lock() {
        _spin_lock.lock();
        while (_locked)
        {
            sleep(&_spin_lock); // 1. 释放_spin_lock并陷入内核放弃cpu执行权
                                // 2. 被唤醒时会重新获得_spin_lock
        }
        _locked = true;
        _spin_lock.unlock();
    }

    unlock() {
        _spin_lock.lock();
        _locked = false;
        wakeup(&_spin_lock); // 唤醒等待在该锁上的其他线程
        _spin_lock.unlock();
    }
};

注释较为丰富,不做详细解释。

2.2 锁与原子操作的区别、关系
  • 锁的实现依赖于原子操作指令
  • 锁的实现需要内存屏障保证临界区指令不跑出临界区,该内存屏障为最严格的内存屏障,任何指令都不可以越过该内存屏障
  • 自旋锁在获取锁的时候,如果获取失败会一直尝试,在锁竞争不严重时效率较高
  • 互斥锁在获取锁失败的时候会sleep,陷入内核;在锁竞争严重是,线程会频繁在内核态与用户态切换,非常影响性能。
  • 在使用锁时提高性能主要是两点:
    1. 减少锁竞争
    2. 缩小临界区

3. C++的6中memory order

6种memory order或者说成6种内存屏障。理论上只要有一种内存屏障就够了,即所有该内存屏障之前的指令不可以重排到内存屏障之后,所有该内存屏障之后的指令不可以排到内存屏障之前,但这种内存屏障太严格了,有时候部分指令越过内存屏障对逻辑正确性无影响,同时又能提高性能。针对这种情况,c++在原子变量的操作中提供了6中内存屏障供开发人员自由选择:

  • memory_order_relaxed: 无内存屏障语义,只保证该操作的原子性。
  • memory_order_consumeload方法使用该内存屏障,内存屏障后面的所有依赖该操作的指令都不可以重排到该内存屏障前面。
```cpp
    a.load(std::memory_order_consume);
    b = a;    // 该指令依赖a的值,所以在该内存屏障语义下,不允许重排到该指令前面
```
  • memory_order_acquireload方法使用该内存屏障,比memory_order_consume更严格,内存屏障后面的所有读写指令都不可以重排到该内存屏障前面。
  • memory_order_releasestore方法使用该内存屏障,内存屏障前面的所有读写指令都不可以重排到该内存屏障后面,经常像下面这样搭配使用。
    1. 线程1使用memory_order_release, 线程2使用 memory_order_acquire, 构成Release-Acquire ordering
    2. 线程1使用memory_order_release, 线程2使用 memory_order_consume, 构成Release-Consume ordering
  • memory_order_acq_relcompare_exchange_strongread-modify-write使用该内存屏障,同时具有memory_order_acquirememory_order_release语义。
  • memory_order_seq_cstload方法使用该内存屏障具有memory_order_acquire语义;store方法使用该内存屏障,具有memory_order_release语义。read-modify-write类型操作如(compare_exchange_strong)使用该内存屏障具有memory_order_acq_rel语义。

4.参考资料

  1. std::memory_order cppreference.com
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值