記憶體順序(memory ordering)
你由上而下寫程式,自然假設機器就按那個順序執行,而且你做的每一次寫入都會立刻被其他每條執行緒看到。這兩個假設在現代硬體上都是錯的。為了速度,CPU 與編譯器被允許重排記憶體的讀與寫,而每顆核心可能先把最近的寫入留在自己的緩衝區裡,過一陣子才讓其他核心看見。記憶體順序,就是描述「哪些重排被允許」以及「一條執行緒的寫入何時保證對另一條可見」的那套規則。在單一執行緒內這一切都觀察不到;跨執行緒時,它卻能產生令人費解的結果。
一個具體的震撼:兩條執行緒,x 與 y 都從 0 開始。執行緒 1 做 x = 1; 然後讀 y。執行緒 2 做 y = 1; 然後讀 x。你也許能「證明」兩者至少有一個必定讀到 1。但在有寫入緩衝的真實 CPU 上,兩條執行緒可能都讀到 0——因為當讀取發生時,各自的寫入都還躺在自己核心的緩衝區裡,尚未對另一方可見。程式順序說的是先寫後讀,可見的順序卻實質上是先讀後寫。這不是硬體錯誤,而是有文件記載的行為,其精確規則由記憶體一致性模型所明定。
記憶體順序,正是為什麼天真的同步程式碼——包括皮特森解法與自製的無鎖技巧——在紙上正確、在多核心機器上卻會失效。它也是為什麼你在日常程式碼裡幾乎從不直接對它推理:寫得正確的互斥鎖、號誌與原子操作都已內含必要的順序保證(acquire/release 語意),所以只要你用它們來保護共用資料,你看到的就是合理、直覺的順序。只有當你跨出這些工具、直接操弄共用變數時,你才必須正面對付赤裸的記憶體順序——而那正是並行錯誤變得惡夢般難纏的時刻。
一個生產者先寫 data = 42、再設 ready = true。一個消費者等到 ready 後才讀 data。在沒有順序保證的情況下,消費者可能看到 ready 變成 true、而 data 卻還是 0,因為那兩次寫入被重排了、或尚未可見——於是它讀到了垃圾值。
程式順序不等於可見順序;核心為了速度會重排並緩衝寫入。
你很少需要直接對記憶體順序推理,因為正確的鎖與原子操作已內嵌正確的屏障。危險地帶是「直接觸碰共用變數」的手寫無鎖程式碼。