在網上看了不少memory order的文章,結果越看越糊塗。本覺得懂了,結果碰到問題仍是不懂。反反覆覆,最終才造成一套能夠邏輯自洽的解釋。記錄在此,既但願減小後來者被錯誤文檔誤導的痛苦,也但願有高手能夠不吝賜教指點一二。android
在闡述memory order以前,首先須要介紹memory model,中文翻譯爲「內存模型」。關於這個概念,網上有很多文不對題的解釋,尤爲是Java的memory model。很多文章將將它理解爲JVM中各塊內存區域的分佈和做用,實際上是張冠李戴了。c++
C++內存模型和Java內存模型同樣,都屬於語言層面的抽象規範。而memory order則屬於其中的子概念,於C++11中正式被引入。它定義了一個原子操做與其附近全部其餘與memory交互的操做之間的重排(reorder)限制。爲了方便理解,其定義能夠被拆分爲如下幾個小點來細化:程序員
對於C++而言,普通開發者是不須要也不會接觸到memory order這個概念的,由於他們被保護得很好。這裏的「保護」指的是:編程
既然如此,那memory order這個概念爲何還會誕生?緣由也有兩個:markdown
Memory order總共有6種類型,但這裏我只準備介紹4種。memory_order_consume和memory_order_acq_rel被排除在外的緣由以下:併發
接下來首先出場的是memory_order_acquire和memory_order_release。memory_order_acquire只用於原子化的load(讀操做)操做,而memory_order_release只用於原子化的store(寫操做)操做。其寫法一般以下所示:函數
std::atomic<int> x;
x.load(std::memory_order_acquire);
x.store(1, std::memory_order_release);
複製代碼
memory_order_acquire禁止了該load操做以後的全部讀寫操做(不論是原子對象仍是非原子對象)被重排到它以前去運行。oop
memory_order_release禁止了該store操做以前的全部讀寫操做(不論是原子對象仍是非原子對象)被重排到它以後去運行。性能
在同一個原子化對象上使用這兩種memory order將會獲得一個額外的好處,即兩個線程在知足某種條件時將會擁有特定的數據可見性。這句話比較拗口,下面用圖示來展開說明。優化
當flag.load在時間上晚發生於flag.store時,Thread 1上flag.store以前的全部操做對於Thread 2上flag.load以後的全部操做都是可見的。若是flag.load發生的時間早於flag.store,那麼兩個線程間則不擁有任何數據可見性。
爲了保證flag.load在時間上晚發生於flag.store,咱們能夠經過if邏輯來進行選擇。所以,下面的寫法將會永遠assert經過。
接着咱們考慮當全部Atomic對象的讀都採用memory_order_acquire,寫都採用memory_order_release時,兩個不一樣Atomic對象的操做之間是否會發生重排。
讀寫操做之間的關係總共有四種:讀讀,讀寫,寫寫,寫讀。對於前三種操做關係,memory_order_acquire和memory_order_release均可以保證兩條針對原子對象的操做不發生重排。但針對最後一種操做關係「寫讀」則沒法保證。緣由是前面一條指令是store,memory_order_release只能保證store以前的指令不重排到store以後,卻沒法禁止位於其後的load指令重排到它前面;後面一條指令是load,memory_order_acquire只能保證load以後的指令不重排到load以前,卻沒法禁止位於其前的store指令重排到它後面。最終store指令將有可能重排到load指令以後,這種沒法禁止的重排關係咱們簡稱爲SL。
Memory_order_acquire和memory_order_release是程度中等的memory order,比他們強一些的就叫作memory_order_seq_cst(sequential consistent),它只比memory_order_release/memory_order_acquire多一個功能,便可以禁止SL的重排。而memory_order_release則至關於自廢武功,兩個不一樣原子對象間的操做能夠隨便重排,它只保證針對同一個原子對象的操做不發生重排。
ART虛擬機實現了本身的Mutex,其中最關鍵的函數即是Mutex::ExclusiveLock和Mutex::ExclusiveUnlock。Android Q以前的Mutex實現代碼以下。
Mutex::ExclusiveLock:
其中最關鍵的操做是①和②:
①表示該Mutex多了一個競爭者,因爲是原子化對象的++操做,所以採用默認的memory order: memory_order_seq_cst。
②是一個RMW(Read-Modify-Write)操做,它會讀取state_的值,並和1進行比較。若是相等則將此線程掛起;若是不相等則直接返回0,讓該線程從新判斷是否能夠得到Mutex。因爲是默認的load操做,所以也採用memory_order_seq_cst。
Mutex::ExclusiveUnlock:
其中最關鍵的操做是③和④:
③也是一個RMW操做,它會讀取state_的值,並和cur_state進行比較。若是相等,則令state_等於0;若是不相等,則返回false。因爲是默認操做,因此load和store都採用memory_order_seq_cst。
④讀取num_contenders的值,可是傳入了memory_order_relaxed,代表對該操做作了最弱的重排限制。
上面的代碼在多數狀況下都沒有問題,可是按照以下的順序執行便會出錯。
Thread 1執行解鎖的操做,Thread 2執行上鎖的操做。
因爲③是RMW操做,實質上能夠拆分爲多條指令,③.a和③.b表示其中的load和store操做。CompareAndSet雖然是原子化操做,可是它只保證在執行過程當中該原子對象的值不會被外界改動。至於其餘指令是否能夠重排到③.a和③.b之間,則由具體的memory order決定。
操做④能夠被重排在③.a和③.b之間的緣由:
雖然③.a和③.b的memory order爲memory_order_seq_cst,可是當重排的另外一個操做不是memory_order_seq_cst修飾的原子化操做時,memory_order_seq_cst便退化成了memory_order_acquire或memory_order_release(取決於操做是load仍是store)。所以③.a只能限定④不重排到它以前,而③.b對④則沒有任何重排限制。所以,④能夠被重排到③.a和③.b之間。
從Thread 1的視角看,①和②都是針對原子對象的操做,所以兩者的執行順序必須等同代碼順序,也即①在②以前執行。另外①②在③.a和③.b中間執行並不影響③的原子性,所以也是被容許的。
一旦程序按照這樣的順序執行,便會致使Thread 2釋放鎖但不喚醒Thread 1,以致於Thread 1一直睡下去。而究其緣由,這一切都是由④重排到③.a和③.b之間致使的。
如今咱們從新思考③.b和④之間的關係。
③.b是一個原子化對象的store操做,④是一個原子化對象的load操做。這是一個典型的SL(store load)情形,而限制他們被重排只有一種方法:將兩者都用memory_order_seq_cst修飾。
所以解決的方案也比較簡單,也即將num_contenders_的memory order改成memory_order_seq_cst。
固然,這麼更改之後會對性能產生必定的影響。由於Mutex在虛擬機中被大量的使用,它任何小小的改動都會影響深遠。正是由於這個緣由,Google在Android Q上對Mutex的實現進行了優化,將num_contenders_和state_合併爲一個原子化對象,這樣就不存在兩個不一樣原子化對象操做之間的重排關係了。合併後的原子化對象叫作state_and_contenders_,其最低位的0或1表明state_,而高位的數字除以2便表明num_contenders_。
具體的change在這裏,感興趣的夥伴能夠繼續研究。