執行緒與並行

並行程式中的不確定性(nondeterminism in concurrent programs)

單執行緒程式有一種令人安心的決定性:給它相同的輸入,它每次都做完全相同的事,一步不差。並行程式失去了這個保證。因為作業系統時時刻刻決定哪條執行緒何時跑——而那取決於時機、負載、以及機器上其他程式——同一個並行程式給同一個輸入,每次執行卻可能走不同的路。這就是不確定性(nondeterminism):結果不單由輸入決定,還由一個你既看不到也控制不了的交錯決定。

機制是這樣。執行緒的操作能以許多種可能順序交錯,而實際發生哪種順序,是由排程器根據你程式以外的因素決定的——CPU 多忙、中斷何時抵達、還有什麼在跑。所以把程式跑兩次,執行緒每次可能交錯得不同。若程式碼在每一種交錯下都正確,這份不確定性無害——你只是經不同路線得到正確答案。但若某個罕見的交錯有錯誤(一個競爭條件),那麼錯誤只在那個特定交錯恰好發生時才出現,那也許是百萬次裡的一次,而且只在你難以重建的條件下。

這為何是並行錯誤如此惱人的深層原因:它們無法可靠地重現。一個競爭可能通過每一項測試、在開發時跑得完美無瑕,然後在正式環境重負載下失敗一次——而當你試圖重現它時,你的除錯改變了時機(加一個中斷點或一句印出會讓某條執行緒慢下來),於是那個有害的交錯不再發生,錯誤彷彿消失了。那就是海森堡蟲(heisenbug):觀察它就改變了它。誠實的結論是:你無法靠把程式跑幾次、沒看到失敗就把並行錯誤測掉——沒崩潰不是正確的證明。你必須對所有交錯進行推理、消除或保護共享可變狀態,並依靠像執行緒消毒器這類能直接偵測資料競爭、而非等它顯現的工具。

跑一個有未同步共享計數器、由四條執行緒遞增的程式。一次印出 4000000、下一次印出 3998732、再下一次 3999981——每次都不同、全都錯,因為執行緒每次執行交錯得不一樣,弄丟了不同數量的更新。

同樣的程式碼、同樣的輸入,每次執行結果不同——並行錯誤的招牌。

「跑了 100 次都沒事」並不代表程式碼正確——那個罕見的交錯可能只是還沒發生。更糟的是,觀察的動作(除錯器、印出、sleep)會因改變時機而把錯誤藏起來。對交錯進行推理、並使用執行緒消毒器,別信賴幸運的幾次執行。

又称
nondeterministic executionwhy concurrency bugs are hard to reproduce不確定性非決定性