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
,陷入内核;在锁竞争严重是,线程会频繁在内核态与用户态切换,非常影响性能。 - 在使用锁时提高性能主要是两点:
- 减少锁竞争
- 缩小临界区
3. C++
的6中memory order
6种memory order或者说成6种内存屏障。理论上只要有一种内存屏障就够了,即所有该内存屏障之前的指令不可以重排到内存屏障之后,所有该内存屏障之后的指令不可以排到内存屏障之前,但这种内存屏障太严格了,有时候部分指令越过内存屏障对逻辑正确性无影响,同时又能提高性能。针对这种情况,c++在原子变量的操作中提供了6中内存屏障供开发人员自由选择:
memory_order_relaxed
: 无内存屏障语义,只保证该操作的原子性。memory_order_consume
:load
方法使用该内存屏障,内存屏障后面的所有依赖该操作的指令都不可以重排到该内存屏障前面。
```cpp
a.load(std::memory_order_consume);
b = a; // 该指令依赖a的值,所以在该内存屏障语义下,不允许重排到该指令前面
```
memory_order_acquire
:load
方法使用该内存屏障,比memory_order_consume
更严格,内存屏障后面的所有读写指令都不可以重排到该内存屏障前面。memory_order_release
:store
方法使用该内存屏障,内存屏障前面的所有读写指令都不可以重排到该内存屏障后面,经常像下面这样搭配使用。- 线程1使用
memory_order_release
, 线程2使用memory_order_acquire
, 构成Release-Acquire ordering
- 线程1使用
memory_order_release
, 线程2使用memory_order_consume
, 构成Release-Consume ordering
- 线程1使用
memory_order_acq_rel
:compare_exchange_strong
等read-modify-write
使用该内存屏障,同时具有memory_order_acquire
和memory_order_release
语义。memory_order_seq_cst
:load
方法使用该内存屏障具有memory_order_acquire
语义;store
方法使用该内存屏障,具有memory_order_release
语义。read-modify-write
类型操作如(compare_exchange_strong
)使用该内存屏障具有memory_order_acq_rel
语义。