經典同步問題與並行程式設計

記憶體一致性模型(memory consistency model)

記憶體一致性模型是這樣一份契約:當多個執行緒讀寫共享記憶體時,它規定一個執行緒被允許觀察到這些讀寫的哪些順序。聽起來似乎理所當然——寫入當然按你寫的順序發生、人人也都那樣看到。令人不安的真相是:在真實硬體上、且經過真實編譯器最佳化後,往往並非如此。為了速度,CPU 與編譯器會重排、緩衝、快取記憶體操作,於是一個執行緒看到另一個執行緒寫入的順序,可能不同於它們被發出的順序。記憶體模型就是那本精確、由語言或硬體定義的規則手冊,說明你能與不能仰賴什麼。

最簡單、最直觀的模型是循序一致性(sequential consistency):它假裝所有執行緒的操作被併進一條全域序列,每個執行緒自己的操作維持在程式順序,而每個執行緒都看到那同一條順序。這就是多數人想像中的並行。麻煩在於到處強制它代價高昂,所以真實系統採用寬鬆(較弱)的記憶體模型,為效能允許更多重排。一個著名的徵兆:兩個執行緒,x 與 y 皆從 0 開始;執行緒 1 設 x = 1 後讀 y,執行緒 2 設 y = 1 後讀 x。在循序一致性下,至少有一個必讀到 1。在寬鬆模型下兩者都可能讀到 0,因為每個執行緒的寫入可能被緩衝、在讀取發生時尚未對另一方可見——一個看似不可能、卻真實存在的結果。

你對此能做的,是插入記憶體屏障(柵欄)或使用帶有明確順序的原子操作,它們告訴硬體與編譯器「不要跨越這一點重排」以及「讓這個寫入在那個讀取之前可見」。更高階的工具——鎖、條件變數、正確使用的原子操作——會替你打包好正確的屏障,這正是多數程式設計師能忽略血淋淋細節的主因:若你用鎖保護所有共享存取,語言便保證你得到看起來循序一致的行為。誠實而重要的提醒是:一旦你寫無鎖程式碼、自製同步、或不用鎖就共享資料,寬鬆記憶體模型瞬間成了你的問題,而正確地推理它,是所有程式設計中最難的事情之一。

x = y = 0。執行緒 1:x = 1; r1 = y。執行緒 2:y = 1; r2 = x。在循序一致性下你永遠不會得到 r1 == 0 且 r2 == 0。在寬鬆模型下卻可能,因為每個寫入可能被緩衝、在另一個執行緒讀取時尚未可見。

「寫入緩衝」的試金石——課本上那個例子:寬鬆記憶體模型允許、但我們的直覺禁止的結果。

若你用同一把鎖保護每一次共享存取,你就能以循序一致性來思考、完全忽略寬鬆模型——鎖會插入必要的屏障。寬鬆模型只有在你手動同步時才會咬你。

又稱
記憶體模型memory model