競爭條件(race condition)
競爭條件是一種錯誤:程式的正確性取決於某些你其實無法掌控的事件時機或順序。兩條執行緒「賽跑」般衝向同一個資源,誰先到決定了結果——有時對、有時錯,全憑運氣。日常的畫面:兩個人在同一瞬間伸手去拿最後一塊餅乾。你拿到、對方拿到、還是兩手相撞餅乾掉地上,完全取決於誰早動了那零點幾秒。
具體地說:每當執行緒的執行結果會隨著它們的操作恰好以何種順序交錯而改變、而程式卻假設了某個並不被保證的特定順序時,競爭條件就存在了。經典案例是兩條執行緒檢查一個條件然後依此行動——「檔案在嗎?不在,那就建立它」——結果兩條都檢查、都發現不在、都去建立。缺陷不在任何單獨一行;每一行都沒問題。缺陷在「檢查」與「行動」之間的空隙,在那期間另一條執行緒能溜進來,讓第一條剛做的決定失效。因為作業系統幾乎能在任何一點切換執行緒,那個空隙是真實且可被踩中的。
競爭條件為何如此陰險:它們依賴時機,所以常常通過每一項測試、好端端跑了好幾個月,然後在重負載下罕見又不可預測地失敗——偏偏在你最承受不起、也最難重現的時候。它們是比資料競爭(下一條)更廣的類別:競爭條件是任何「正確性取決於時機」的錯誤,即使沒有低階記憶體衝突,例如用了各自完全安全的操作、但順序可被打斷的「先檢查再行動」。解法是讓時機不再重要——讓那段相關的步驟序列成為一個不可分割的單位來發生,通常靠同步機制。
兩條執行緒都跑:if (balance >= 100) balance -= 100;。兩者在任一方扣款前都把 balance 讀成 100 並通過檢查。接著兩者都扣,balance 最後成了 -100——一筆任一次檢查都不該允許的透支。檢查與扣款沒有當成一個不可分割的步驟來做。
一個「先檢查再行動」的競爭:檢查與行動之間的空隙讓另一條執行緒改變了世界。
並非每個競爭條件都是資料競爭,也並非每個資料競爭每次都造成明顯錯誤的結果——這正是它們難纏之處。「它通過測試了」在這裡什麼也不能證明;競爭可以潛伏很久卻仍在那裡埋伏。