閱讀663 返回首頁    go 阿裏雲 go 技術社區[雲棲]


內存屏障

本文我將和大家討論並發編程中最基礎的一項技術:內存屏障或內存柵欄,也就是讓一個CPU處理單元中的內存狀態對其它處理單元可見的一項技術。

CPU使用了很多優化技術來實現一個目標:CPU執行單元的速度要遠超主存訪問速度。在上一篇文章 “Write Combing (合並寫)”中我已經介紹了其中的一項技術。CPU避免內存訪問延遲最常見的技術是將指令管道化,然後盡量重排這些管道的執行以最大化利用緩存,從而把因為緩存未命中引起的延遲降到最小。

當一個程序執行時,隻要最終的結果是一樣的,指令是否被重排並不重要。例如,在一個循環裏,如果循環體內沒用到這個計數器,循環的計數器什麼時候更 新(在循環開始,中間還是最後)並不重要。編譯器和CPU可以自由的重排指令以最佳的利用CPU,隻要下一次循環前更新該計數器即可。並且在循環執行中, 這個變量可能一直存在寄存器上,並沒有被推到緩存或主存,這樣這個變量對其他CPU來說一直都是不可見的。

CPU核內部包含了多個執行單元。例如,現代Intel CPU包含了6個執行單元,可以組合進行算術運算,邏輯條件判斷及內存操作。每個執行單元可以執行上述任務的某種組合。這些執行單元是並行執行的,這樣指 令也就是在並行執行。但如果站在另一個CPU角度看,這也就產生了程序順序的另一種不確定性。

最後,當一個緩存失效發生時,現代CPU可以先假設一個內存載入的值並根據這個假設值繼續執行,直到內存載入返回確切的值。

1 CPU核
2 |
3 V
4 寄存器
5 |
6 V
7 執行單元 -> Load/Store緩衝區->L1 Cache --->L3 Cache-->內存控製器-->主存
8 | |
9 +-> Write Combine緩衝區->L2 Cache ---+

代碼順序並不是真正的執行順序,隻要有空間提高性能,CPU和編譯器可以進行各種優化。緩存和主存的讀取會利用load, store和write-combining緩衝區來緩衝和重排。這些緩衝區是查找速度很快的關聯隊列,當一個後來發生的load需要讀取上一個 store的值,而該值還沒有到達緩存,查找是必需的,上圖描繪的是一個簡化的現代多核CPU,從上圖可以看出執行單元可以利用本地寄存器和緩衝區來管理 和緩存子係統的交互。

在多線程環境裏需要使用某種技術來使程序結果盡快可見。這篇文章裏我不會涉及到 Cache Conherence 的概念。請先假定一個事實:一旦內存數據被推送到緩存,就會有消息協議來確保所有的緩存會對所有的共享數據同步並保持一致。這個使內存數據對CPU核可見 的技術被稱為內存屏障或內存柵欄

內存屏障提供了兩個功能。首先,它們通過確保從另一個CPU來看屏障的兩邊的所有指令都是正確的程序順序,而保持程序順序的外部可見性;其次它們可以實現內存數據可見性,確保內存數據會同步到CPU緩存子係統。

大多數的內存屏障都是複雜的話題。在不同的CPU架構上內存屏障的實現非常不一樣。相對來說Intel CPU的強內存模型比DEC Alpha的弱複雜內存模型(緩存不僅分層了,還分區了)更簡單。因為x86處理器是在多線程編程中最常見的,下麵我盡量用x86的架構來闡述。

Store Barrier

Store屏障,是x86的”sfence“指令,強製所有在store屏障指令之前的store指令,都在該store屏障指令執行之前被執行,並把store緩衝區的數據都刷到CPU緩存。這會使得程序狀態對其它CPU可見,這樣其它CPU可以根據需要介入。一個實際的好例子是Disruptor中的BatchEventProcessor。當序列Sequence被一個消費者更新時,其它消費者(Consumers)和生產者(Producers)知道該消費者的進度,因此可以采取合適的動作。所以屏障之前發生的內存更新都可見了。

01 private volatile long sequence = RingBuffer.INITIAL_CURSOR_VALUE;
02 // from inside the run() method
03 T event = null;
04 long nextSequence = sequence.get() + 1L;
05 while (running)
06 {
07     try
08     {
09         final long availableSequence = barrier.waitFor(nextSequence);
10         while (nextSequence <= availableSequence)
11         {
12             event = ringBuffer.get(nextSequence);
13             boolean endOfBatch = nextSequence == availableSequence;
14             eventHandler.onEvent(event, nextSequence, endOfBatch);
15             nextSequence++;
16         }
17         sequence.set(nextSequence - 1L);
18         // store barrier inserted here !!!
19     }
20     catch (final Exception ex)
21     {
22         exceptionHandler.handle(ex, nextSequence, event);
23         sequence.set(nextSequence);
24         // store barrier inserted here !!!
25         nextSequence++;
26     }
27 }

Load Barrier

Load屏障,是x86上的”ifence“指令,強製所有在load屏障指令之後的load指令,都在該 load屏障指令執行之後被執行,並且一直等到load緩衝區被該CPU讀完才能執行之後的load指令。這使得從其它CPU暴露出來的程序狀態對該 CPU可見,這之後CPU可以進行後續處理。一個好例子是上麵的BatchEventProcessor的sequence對象是放在屏障後被生產者或消 費者使用。

Full Barrier

Full屏障,是x86上的”mfence“指令,複合了load和save屏障的功能。

Java內存模型

Java內存模型volatile變量在寫操作之後會插入一個store屏障,在讀操作之前會插入一個load屏障。一個類的final字段會在初始化後插入一個store屏障,來確保final字段在構造函數初始化完成並可被使用時可見。

原子指令和Software Locks

原子指令,如x86上的”lock …” 指令是一個Full Barrier,執行時會鎖住內存子係統來確保執行順序,甚至跨多個CPU。Software Locks通常使用了內存屏障或原子指令來實現變量可見性和保持程序順序。

內存屏障的性能影響

內存屏障阻礙了CPU采用優化技術來降低內存操作延遲,必須考慮因此帶來的性能損失。為了達到最佳性能,最好是把要解決的問題模塊化,這樣處理器可 以按單元執行任務,然後在任務單元的邊界放上所有需要的內存屏障。采用這個方法可以讓處理器不受限的執行一個任務單元。合理的內存屏障組合還有一個好處 是:緩衝區在第一次被刷後開銷會減少,因為再填充改緩衝區不需要額外工作了。


文章轉自 並發編程網-ifeve.com

最後更新:2017-05-22 18:01:41

  上一篇:go  從Java視角理解係統結構(三)偽共享
  下一篇:go  Write Combining