執行緒與並行

資料競爭(data race)

資料競爭是一種具體、技術性的麻煩,比一般的競爭條件更窄。它發生在:兩條或更多執行緒同時存取同一個記憶體位置、其中至少一次是寫入、且沒有任何東西強制它們之間的順序(沒有同步)。想像兩個人同時把不同的字寫進表單上同一個空格——墨水糊在一起,你說不出那格現在是什麼。在 C 與 C++ 裡,這不只是「你拿到什麼未定」;標準明定資料競爭是未定義行為,那糟得多。

讓我們把三個條件講精確,因為三者必須同時成立。第一,兩條執行緒碰同一個位置——同一個變數、共享物件的同一個位元組。第二,至少一次存取是寫入;兩條執行緒只讀同一筆資料永遠不會競爭。第三,存取是並行且未同步的——沒有任何東西(沒有互斥鎖、沒有原子操作、沒有別的順序保證)確立其中一個先於另一個發生。當三者都成立,程式就有資料競爭。注意它與競爭條件的對比:競爭條件是任何依賴時機的正確性錯誤;資料競爭是「未同步的、相衝突的記憶體存取」這個具體的低階情形。每個資料競爭都是競爭條件,但競爭條件可以完全沒有資料競爭(例如,全由各自安全的操作組成的「先檢查再行動」)。

這在 C 與 C++ 為何如此危險:因為標準把資料競爭稱作未定義行為,編譯器被允許假設它從不發生。它於是可能以讓這錯誤變得詭異的方式最佳化——把指令重排、把一個共享變數快取在暫存器裡使另一條執行緒的更新永遠看不到、或產出「不可能」的值。所以資料競爭不只是「你可能讀到一個有點過時的數字」;它能以徹底擊潰你推理的方式弄壞程式。唯一正確的修法是移除競爭——同步那些相衝突的存取(一把鎖或一個原子操作),讓衝突再也無法發生。一個叫做執行緒消毒器(thread sanitizer)的工具能在執行期偵測許多資料競爭,非常值得使用。

兩條執行緒各自對一個普通 int 做 shared_counter++ 而不上鎖。那就是資料競爭:同一位置、都在寫、無同步——在 C 中是未定義行為。把 shared_counter 改成原子型別、或用互斥鎖守住它,就移除了競爭。

同一位置 + 至少一次寫入 + 無同步 = 資料競爭 = C/C++ 中的未定義行為。

因為資料競爭是未定義行為,「它看起來能動」毫無意義——編譯器可能在假設競爭不會發生的前提下最佳化了你的程式碼,所以失敗可能出現在離競爭那行很遠之處、或只在 -O2 下出現。別用 sleep() 或重排程式碼來「修」它;只有真正的同步才能移除它。

又稱
data hazard on shared memory資料競態