1. 优化带来的烦恼
用过GCC编译的同学应该知道GCC有O0、O1、O2、O3等优化选项,启用这些选项往往可以提高程序的运行效率,但它并不是万无一失的,尤其是在多线程场景下。而这些优化背后的技术正是指令重排。因为编译器或处理器也很难确定代码逻辑的原本意图。
锁能够保持原子性,但是经过编译器优化之后的代码,并不是绝对时序正确的,况且处理器还有可能进一步优化。这里面最经典的一个例子就是单例模式,Double-Checked Locking is Fixed In C++11 。
2. 内核提供的解决方案
内核提供以下方法,阻止编译器和处理器进行指令重排
- mb() rmb() wmb() 会将硬件内存屏障插入到代码中。rmb用于读访问内存屏障,wmb用于写访问屏障,mb兼具二者。读屏障插入到代码中之后,保证屏障之前的读操作代码结束之后,屏障之后的读操作代码才读。
- barrier 插入优化屏障。屏障之前所有有效的内存地址,在屏障之后都将失效。也就是屏障之后,不能再读写了。
- smb_mb() smb_rmb() smb_wmb() 在SMP系统产生硬件内存屏障,如果用在单处理器系统上产生的将是软件屏障。
- read_barrier_depends 会考虑读操作之间的依赖性,设置读访问屏障。
屏障肯定是会影响性能的,但总不能为了优化性能而让程序出现错误。在任何程序面前,正确性永远是第一位的。
说明两个概念
内存屏障:rmb(), wmb(), mb(),可以防止硬件上的指令重排,针对的是处理器CPU。
优化屏障:barrier(),避免编译器对内存访问的优化,针对的是编译器。
它们在很多地方称为内存栅栏,但英文的话都是memory barrier。
3. 典型的应用
3.1 安全的单例模式
Double-Checked Locking 咋一眼看山去没什么问题,但是考虑下指令重排,就会发现问题。如果返回指针在new之前,那是我们不愿看到的,而这里使用屏障可以解决这个问题。
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance;
...// insert memory barrier
if (tmp == NULL) {
Lock lock;
tmp = m_instance;
if (tmp == NULL) {
tmp = new Singleton;
...// insert memory barrier
m_instance = tmp;
}
}
return tmp;
}
3.2 内核抢占
preempt_disable();
do_something();
preempt_enable();
在内核抢*,preempt_disable对计数器加1,也就是告诉其他线程,不要来抢这个是我的, preempt_enable反之。
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while(0)
#define preempt_enable() \
do { \
barrier(); \
preempt_check_resched(); \
} while(0)
参考:
[0] 深入Linux内核架构
[1] https://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/