執行緒與並行

讀-改-寫危害(read-modify-write hazard)

這裡是「並行為何會出錯」最重要的一個具體例子。看似無害的陳述式 count++ 並非一個動作——而是三個:從記憶體讀出 count 目前的值、把它加一、再把新值寫回去。我們稱之為讀-改-寫(read-modify-write)。它在你的原始碼裡看似不可分割,但底下其實是三個各自獨立的步驟,作業系統能在其中任何兩步之間切換到另一條執行緒。那個空隙就是危害。

用兩條執行緒 A 和 B 走一遍這個失敗,兩者都在 count 為 5 時做 count++。A 讀到 5。在 A 寫回之前,作業系統切到 B。B 讀到 5(寫入還沒發生),加一得 6,寫入 6。現在作業系統切回 A,A 仍握著它先前讀到的 5,加一得 6,寫入 6。發生了兩次遞增,但 count 從 5 變成 6,而非 7。一次更新被默默地弄丟了——「遺失的更新」。原因是 A 的讀-改-寫和 B 的讀-改-寫交錯了,而非各自整個完成。原始碼裡沒有任何東西說「這裡別打斷我」,所以作業系統有自由在最糟的時刻交錯。

這為何重要、又如何修正:讀-改-寫是無數並行錯誤的種子,因為太多自然的操作其實偷偷是讀-改-寫——把計數器加一、往共享串列附加、更新餘額、切換一個旗標。修法是讓那三步成為一個不可分割的單位。兩種標準方式:用互斥鎖守住的臨界區間把它們包起來(這樣一次只有一條執行緒做這次讀-改-寫),或使用原子操作——一種有硬體支援的特殊指令,把讀-改-寫當成單一、不可被打斷的步驟來執行(Field n 的主題)。關鍵的領悟是:「一行 C」不等於「機器上的一步」,而機器的步驟正是執行緒相撞之處。

在許多 CPU 上,count++ 大致編譯成三條指令:把 count 載入一個暫存器、把暫存器加一、把暫存器存回 count。在載入與存回之間發生一次執行緒切換,就足以弄丟一次更新。

count++ 是讀-改-寫:三個步驟,任兩步之間都可被打斷,所以兩條執行緒可能弄丟一次更新。

把變數宣告為 volatile 並不能讓 count++ 安全——volatile 阻止某些編譯器最佳化,但不會讓讀-改-寫變成原子的。你需要的是互斥鎖或真正的原子操作,而非 volatile。

又称
why count++ is not atomicRMW hazard非原子的遞增