多執行緒之重排序詳解
重排序
重排序是指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段。
資料依賴性
如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在資料的依賴性。資料依賴分為3中型別,如下表所示:
上面3中情況,只要重排序兩個操作的順序。程式的結果就會改變。編譯器和處理器是可以對操作進行重排序的,在重排序時,會遵守資料依賴性,編譯器和處理器不會改變存在資料依賴關係的兩個操作的執行順序。這裡說的資料依賴性只針對單個處理器中執行的指令序列和單個執行緒中執行的操作,不同處理器之間和不同執行緒之間的資料依賴是不能被編譯器和處理器考慮的。
as-if-serial語義
as-if-serial語義的意思是:不管怎樣重排序,當然我們知道,重排序的目的是為了提高編譯器和處理器的並行度,單執行緒的程式的執行結果不會被改變,這一點要注意,上面也提到了,這裡的重排序針對的是單執行緒情況。編譯器,runtime和處理器都必須遵守as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在資料依賴關係的操作做重排序,因為這種重排序會改變程式執行結果。但是,如果操作之間不存在資料依賴關係,這些操作就可以被編譯器和處理器重排序。
看如下程式碼:
double pi = 3.14; // A double r = 1.0; //B double area = pi * r * r; //C
根據上面的程式碼我們可以分析:A與C之間存在資料依賴關係,所以C不能排到A的前面,同時B與C之間也存在資料依賴關係,所以,C也不能排到B的前面,但是A與B之間是不存在資料依賴關係的,所以A與B之間是可以進行重排序的。
as-if-serial語義把單執行緒程式保護起來,遵守as-if-serial語義的編譯器,runtime和處理器共同為編寫單執行緒程式的程式設計師建立一個幻覺:單執行緒程式是按照程式的順序來執行的。as-if-serial語義使單執行緒程式設計師無需擔心重排序會干擾他們,也不用擔心記憶體的可見性。
程式順序規則
根據happens-before的程式規則,上面的計算圓的示例程式碼存在3個happens-before關係:
A happens-before B ; B happens-before C; A happens-before C;
這裡第三個happens-before是根據happens-before規則的傳遞性推匯出來的,這很容易理解。這裡A happens-before B ,但實際執行的時候B可能排在A前面,如果 A happens-before B,JMM並不要求A 一定要在 B之前。JMM 僅僅要求前一個操作(操作的結果)對後面的操作可見;而且重排序操作A和操作B後的執行結果與操作A和操作B按happens-before 順序執行的結果是一致的。JMM會認為這種重排序並不是非法的,JMM允許這種重排序。
在計算機中,軟體技術和硬體技術有一個共同的目標:在不改變程式的執行結果的前提下,儘可能提高並行度。處理器與編譯器遵從這一目標,從happens-before的定義我們可以看出,JMM同樣遵守。
重排序對多執行緒的影響
首先看一下程式碼例項:
class RecorderExample{
int a = 0;
boolean flag = false;
public void writer(){
a = 1; // 1
flag = true; // 2
}
public void reader(){
if(flag){ // 3
int i = a * a; // 4
......
}
}
}
flag是一個變數,用來表示變數a是否已被寫入。這裡假設有兩個執行緒A和B ,A執行緒首先執行writer方法,隨後執行緒B執行reader方法。執行緒B在執行操作4的時候,能否看到執行緒A在操作共享變數a的寫入呢?
答案是:在多執行緒的情況下,不一定能看到;
由於操作1和操作2沒有資料依賴的關係,編譯器和處理器可以對這兩個操作進行重排序,操作3和操作4沒有資料依賴關係,編譯器和處理器也可以對其進行重排序,下面我們看一下可能的執行情況的示意圖:
如上所示,操作1 和操作2 進行了重排序。程式執行時,執行緒A首先寫標記變數flag,隨後執行緒B讀這個變數。由於判斷條件為真,執行緒B將讀取變數a。此時,變數a還沒有被執行緒A寫入,所以在這裡,多項層程式的語義就被重排序破壞了。
下面在看一下操作3和操作4重排序會發生什麼效果:
在程式中,操作3和操作4存在控制依賴關係。當代碼中存在控制依賴行時,會影響指令序列執行的並行度。為此,編譯器和處理器會採用猜測執行來克服控制相關性對並行度的影響。以處理器的猜測執行為例,執行執行緒B的處理器可以提前讀取並計算a*a,然後把計算結果臨時儲存到一個名為重排序緩衝的硬體快取中。當操作3的條件判斷為真的時候,就把該結算結果寫入到變數i中。
從上圖我們可以看出,猜測執行實質上是對操作3和操作4進行了重排序,重排序在這裡破壞了多執行緒程式的語義。
在單執行緒程式中,對存在控制依賴的操作進行重排序,不會改變執行結果(這也是as-if-serial 語義允許對存在控制依賴的操作做重排序的原因),但是在多執行緒的程式中,對存在控制依賴的操作重排序,可能會改變程式的執行結果。