1. 程式人生 > 實用技巧 >volatile底層實現原理

volatile底層實現原理

記憶體屏障

原文地址 作者:Martin Thompson 譯者:一粟 校對:無葉,方騰飛

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

CPU使用了很多優化技術來實現一個目標:CPU執行單元的速度要遠超主存訪問速度。在上一篇文章 “[Write Combing](http://ifeve.com/memory-barriers-or-fences/Write Combing) (合併寫)”中我已經介紹了其中的一項技術。CPU避免記憶體訪問延遲最常見的技術是將指令管道化,然後儘量重排這些管道的執行以最大化利用快取,從而把因為快取未命中引起的延遲降到最小。

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

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

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

CPU核
|
V
暫存器
|
V
執行單元 -> Load/Store緩衝區->L1 Cache --->L3 Cache-->記憶體控制器-->主存
| |
+-> Write Combine緩衝區->L2 Cache ---+

程式碼順序並不是真正的執行順序,只要有空間提高效能,CPU和編譯器可以進行各種優化。快取和主存的讀取會利用load, storewrite-combining緩衝區來緩衝和重排。這些緩衝區是查詢速度很快的關聯佇列,當一個後來發生的load

需要讀取上一個store的值,而該值還沒有到達快取,查詢是必需的,上圖描繪的是一個簡化的現代多核CPU,從上圖可以看出執行單元可以利用本地暫存器和緩衝區來管理和快取子系統的互動。

在多執行緒環境裡需要使用某種技術來使程式結果儘快可見。這篇文章裡我不會涉及到 Cache Conherence 的概念。請先假定一個事實:一旦記憶體資料被推送到快取,就會有訊息協議來確保所有的快取會對所有的共享資料同步並保持一致。這個使記憶體資料對CPU核可見的技術被稱為記憶體屏障或記憶體柵欄

記憶體屏障提供了兩個功能。首先,它們通過確保從另一個CPU來看屏障的兩邊的所有指令都是正確的程式順序,而保持程式順序的外部可見性;其次它們可以實現記憶體資料可見性,確保記憶體資料會同步到CPU快取子系統。

大多數的記憶體屏障都是複雜的話題。在不同的CPU架構上記憶體屏障的實現非常不一樣。相對來說Intel CPU的強記憶體模型比DEC Alpha的弱複雜記憶體模型(快取不僅分層了,還分割槽了)更簡單。因為x86處理器是在多執行緒程式設計中最常見的,下面我儘量用x86的架構來闡述。

Store Barrier

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

private volatile long sequence = RingBuffer.INITIAL_CURSOR_VALUE;
// from inside the run() method
T event = null;
long nextSequence = sequence.get() + 1L;
while (running)
{
    try
    {
        final long availableSequence = barrier.waitFor(nextSequence);
        while (nextSequence <= availableSequence)
        {
            event = ringBuffer.get(nextSequence);
            boolean endOfBatch = nextSequence == availableSequence;
            eventHandler.onEvent(event, nextSequence, endOfBatch);
            nextSequence++;
        }
        sequence.set(nextSequence - 1L);
        // store barrier inserted here !!!
    }
    catch (final Exception ex)
    {
        exceptionHandler.handle(ex, nextSequence, event);
        sequence.set(nextSequence);
        // store barrier inserted here !!!
        nextSequence++;
    }
}

Load Barrier

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

Full Barrier

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

Java記憶體模型

volatile變數在寫操作之後會插入一個store屏障,在讀操作之前會插入一個load屏障

一個類的final欄位會在初始化後插入一個store屏障,來確保final欄位在建構函式初始化完成並可被使用時可見。

原子指令和Software Locks

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

記憶體屏障的效能影響

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

轉載自併發程式設計網 – ifeve.com本文連結地址: 記憶體屏障