操作的交錯執行(interleaving of operations)
當兩條執行緒在一個核心上跑時,作業系統不會先跑完整條 A 再跑整條 B。它跑 A 的幾步、切換、跑 B 的幾步、再切回來,如此反覆。真正執行的這串合併步驟序列——A 與 B 的操作在時間上摻在一起——就叫做交錯(interleaving)。想像兩疊撲克牌被交叉洗成一疊:每張牌在自己那疊裡仍保持原有順序,但兩疊最後以某個你沒選的順序織在一起。
關鍵在於:作業系統可以在機器指令之間幾乎任何邊界處交錯,而你通常無法控制在哪裡。所以對任兩條執行緒而言,存在許多種可能的交錯,而你的程式必須在「每一種」交錯下都正確。哪怕只有一種糟糕的交錯產生錯誤答案,你就有錯誤——而作業系統有自由在你服務最忙的那天,偏偏挑中那個交錯。這就是為什麼推理並行程式碼很難:你不能只追蹤一次執行;你必須考慮這些步驟可能被織在一起的所有方式。讀-改-寫的失敗,正是兩個 count++ 操作的一種特定糟糕交錯。
這個視角為何如此有用:「這段程式碼正確嗎?」變成「這段程式碼在每一種交錯下都正確嗎?」,這立刻暴露了共享可變狀態的危險。它也解釋了解法為何是縮小「會造成影響的交錯」的集合:當你用鎖把臨界區間包起來,你就禁止了「兩條執行緒同時在裡面」的那些交錯,只留下安全的。它還解釋了並行錯誤為何如此難重現——那個特定有害的交錯,可能在百萬次執行中才出現一次,取決於你無法指定的時機。
執行緒 A 的步驟 a1,a2,a3 和執行緒 B 的步驟 b1,b2,b3,可能以 a1,a2,a3,b1,b2,b3 跑(沒問題),也可能以 a1,b1,b2,b3,a2,a3 跑(B 完整地溜進 A 的第一步與第二步之間)。不同的交錯,可能造成不同的結果——而你無權選擇哪一個會發生。
可能有許多種交錯;正確的程式碼必須在它們全部之下都存活。
加上 sleep() 或印出語句會改變時機,因而改變你觀察到的交錯——這就是為什麼一個錯誤可能在你想觀察它的那一刻消失(海森堡蟲)。錯誤其實還在;你只是讓那個有害的交錯變得更罕見。