經典同步問題與並行程式設計

並行臭蟲分類(taxonomy of concurrency bugs)

並行臭蟲自成一類,惡名昭彰,因為它們會躲。循序臭蟲是可重現的:同樣的輸入、同樣的錯誤答案,每次皆然。並行臭蟲取決於執行緒的確切交錯,而排程器每次執行的決定都不同,於是程式可能正確運作一千次,卻在第一千零一次失敗。這類臭蟲被暱稱為海森堡蟲(Heisenbug),因為觀察它們的動作——加一行印出語句、在除錯器下執行、把速度放慢——會改變時序,往往使它們消失。為這些臭蟲反覆出現的形狀取名,是獵捕它們的第一步。

對真實軟體的研究,把多數非死結的並行臭蟲歸入幾個家族。資料競爭(data race)最原始:兩個執行緒並行存取同一個記憶體位置、其中至少一個在寫、且彼此間毫無同步——結果取決於誰勝出,在許多語言裡是未定義的。原子性違反(atomicity violation)更微妙:每個個別存取都有妥善同步,但程式設計師以為會作為一個整體發生的一串動作,在中途被打斷——例如檢查一個指標非空、然後使用它,卻有別的執行緒在縫隙裡把它設為空(檢查後行動的臭蟲,其中檢查時點到使用時點的競態是經典)。順序違反(order violation)是程式假設 A 永遠先於 B 發生,卻沒有任何東西強制它,於是在倒楣的排程上 B 先跑——例如在某執行緒實際執行之前,就使用它本應初始化的變數。死結(資源的循環等待)是第四個大家族,通常獨立研究。

使這個分類具有實用價值的,是每個家族都指向它自己的偵測與預防戰術。資料競爭可由動態競爭偵測器(如 ThreadSanitizer)抓出,它監看每一次共享存取;原子性與順序違反更難,需要謹慎的、基於不變量的推理或專門工具。由於一般測試在這裡如此不可靠——一個通過的測試只證明某一種交錯能用,而非全部——團隊轉而倚賴壓力測試、決定性排程器與重播、徹底探索交錯的模型檢驗,以及最重要的——有紀律的設計(清楚的鎖定慣例、不可變性、訊息傳遞),從建構上使整類臭蟲不可能發生。誠實的結論是:你無法靠測試可靠地把並行臭蟲清掉;你必須在設計上把它們排除掉。

原子性違反:if (p != null) p.use()。執行緒 1 通過了非空檢查;在它呼叫 p.use() 之前,執行緒 2 設 p = null;接著執行緒 1 對空值解參考而當掉。每個存取單獨看都沒問題——錯在假設「檢查後使用」是一個不可分割的步驟。

一個「檢查後行動」的原子性違反:別的執行緒就趁測試與行動之間的縫隙鑽進來。

一整排綠燈的測試並不證明並行的正確性——它只顯示碰巧跑到的那些交錯沒問題。這些臭蟲必須在設計上排除(清楚的鎖定、不可變性、訊息傳遞),並用競爭偵測器與模型檢驗來追捕,而非僅靠測試清掉。

又稱
並行錯誤分類concurrency bug categoriesHeisenbug