競爭條件(race condition)
兩個人共用一個餘額為 100 的銀行帳戶。兩人同一瞬間決定各存 50。每人讀餘額(100)、各加 50、各寫回 150。最終餘額是 150,而非正確的 200——有一筆存款消失了,因為兩個操作以無人預期的方式交錯了。這就是競爭條件:當一項計算的結果取決於兩個或更多平行動作觸碰同一資料的不可預測時序、且其中至少一個在寫入時。
在平行程式中,每當多條執行緒不經協調地讀取並更新一個共享變數,同樣的臭蟲就出現。麻煩在於「把計數器加一」在真機上不是一個不可分割的步驟——它是讀值、加一、寫回,三個步驟,系統可在執行緒間自由交錯。若執行緒 A 讀了,接著執行緒 B 讀到同一個舊值,然後兩者都寫,就丟了一次更新。解藥是同步:一把鎖(mutex)一次只放一條執行緒進臨界區、一個原子運算把讀-改-寫當成一個不可分割的單元、或一種重構——給每條執行緒自己私有的累加器、只在最後合併它們(平行歸約的標準模式)。每種都有代價——鎖會序列化、可能扼殺擴展;過度同步可能抹掉你本想要的平行性。
競爭條件是共享記憶體平行的招牌危險,它們狠毒正因為它們是非確定性的:程式可能給對答案一千次、在第一千零一次給錯,取決於隨負載、核心數、或月相而變的時序。它們在測試中不會可靠地現身,能藏匿數年。這種非確定性在數值上還有一張更微妙的臉:即使是一個正確同步的平行求和,也可能逐次執行給出略微不同的結果,因為部分和的合併順序會變、而浮點加法不具結合律——所以逐位元可重現性是與正確性分開的、真實的考量。
八條執行緒各自對自己那片陣列跑 sum += a[i],全都更新那一個共享的 sum。沒有保護時,更新相撞,總和算出來偏小且每次執行都不同。解法:給每條執行緒一個私有的部分和,最後一次性合併這八個部分和(一次歸約)——沒有相撞、總和正確、完整的平行性。
未同步的共享更新會非確定性地丟資料;私有部分和加上一次歸約即可修正。
競爭條件是非確定性的,所以通過一次測試什麼都證明不了——臭蟲可能只在某種特定時序、核心數或負載下出現。而且請注意,即使無競爭的平行求和也不是逐位元可重現的,因為浮點加法的順序會變、而該加法不具結合律;正確性與可重現性是兩個不同的問題。