同步

記憶體順序與可見性問題

你或許以為:當一個執行緒把一個值寫進記憶體,另一個執行緒會立刻看到它,而且寫入會按你寫的順序發生。在現代多核心硬體上,這兩個假設都不安全。為了求快,編譯器可能重排不相干的記憶體操作,而每個 CPU 核心都有自己的快取與寫入緩衝區,所以一個核心的寫入在對另一個核心變得可見之前可能被延遲——而且寫入變得可見的順序,可能與它們發出的順序不同。這就是記憶體順序與可見性問題。

一個具體的陷阱:執行緒 A 做 data = 42; 然後 ready = 1;。執行緒 B 自旋到 ready == 1,然後讀 data,期望是 42。沒有同步時,執行緒 B 可能觀察到 ready == 1,卻仍看到 data 的舊值——因為 A 的那兩個寫入對 B 變得可見的順序顛倒了,或 B 在它快取裡看到的 data 是過時的。硬體沒有「壞掉」;它正是在做記憶體模型所允許的事。解法是一個記憶體順序約束,常透過一道記憶體屏障(fence)來禁止某些重排,或透過帶指定順序(取得/釋放語意)的原子操作。這正是「鎖做的事不只是互斥」的深層原因:解鎖會執行一次釋放,把你在臨界區間裡所做的所有寫入都公布出去;上鎖會執行一次取得,讓那些寫入對下一個執行緒可見。一把鎖既排除其他執行緒,也公布你的寫入。

這很重要,因為它解釋了你為何不能只「用一個普通旗標」在執行緒間遞交資料,也解釋了正確的同步既關乎可見性、也關乎排除。對多數程式設計者而言有個好消息:只要你用互斥鎖保護所有共享資料(或使用帶預設順序一致性的原子操作),函式庫與硬體就會替你插入正確的屏障,你永遠不必直接推敲重排。危險區是手刻的無鎖程式碼與聰明的旗標花招,在那裡你必須明確地推敲「哪些寫入在何時被公布給誰」——這正是為什麼本條目只點出問題、由第二卷完整處理記憶體模型。

執行緒 A:data = 42; ready = 1; 執行緒 B:while (!ready) {} use(data);。用普通 int 時,B 可能看到 ready == 1 卻讀到過時的 data。在兩者外圍加鎖、或使用帶取得/釋放的原子操作,就保證「看到 ready == 1」也意味著「看到 data == 42」。

鎖不只排除其他執行緒,還公布你的寫入;這就是為什麼一個普通旗標並不安全。

重排與過時的視圖是硬體與編譯器記憶體模型所允許的;它們不是晶片的錯。只要你用互斥鎖守護所有共享資料、或使用順序一致的原子操作,屏障就替你處理好了——只有手刻的無鎖程式碼才逼你直接推敲順序。而且 C 的 volatile 並不是執行緒同步工具;它不提供所需的屏障。

又称
memory orderingvisibilitymemory modelmemory barrier記憶體順序可見性