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/