併發相關的錯誤類型 (Concurrency Bug Types)

Overview Table

概念 重點
兩大類併發 bug 不必要的阻塞 (unwanted blocking) 與 競爭條件 (race condition)
不必要的阻塞 死結 (deadlock)、活結 (livelock)、I/O 阻塞/外部輸入
競爭條件 資料競爭 (data race)、破壞不變量、生命週期問題
症狀差異 阻塞型 → 掛起、無回應;競爭型 → 隨機崩潰、錯誤輸出
除錯難點 競爭型 bug 時序相依,難以重現與定位(共享記憶體的詛咒)

併發 bug 分類總覽

併發程式的錯誤五花八門,但與併發直接相關的錯誤可歸為兩大類,再細分為六個子類型:

併發相關的 bug
├── 不必要的阻塞 (unwanted blocking)
│   ├── 死結 (deadlock)     ── 執行緒互相等待,永久阻塞
│   ├── 活結 (livelock)     ── 迴圈持續檢查(如自旋鎖),CPU 居高不下
│   └── I/O 阻塞 / 外部輸入 ── 等待可能永不到來的輸入
└── 競爭條件 (race condition)
    ├── 資料競爭 (data race) ── 未同步存取共享記憶體 → 未定義行為
    ├── 破壞不變量           ── 懸空指標、雙重釋放、局部更新
    └── 生命週期問題         ── 執行緒存取已銷毀/轉移的資料

不必要的阻塞 (Unwanted Blocking)

執行緒阻塞時無法處理任何任務,因為它在等待某個「條件」達成——通常是互斥鎖 (mutex)條件變數 (condition variable)futureI/O 操作。阻塞本身是多執行緒程式的先天特性;之所以「不必要」,是因為其他執行緒也在等待這個被阻塞的執行緒完成某些操作,於是阻塞會連鎖擴散。

類型 定義 症狀 範例
死結 (deadlock) 兩個(或多個)執行緒互相等待對方持有的資源(見第3章) 任務擱置;UI 執行緒死結 → 介面無回應;或介面仍可回應但部分任務無法完成(查詢無結果、文件不列印) 兩執行緒以相反順序鎖住兩把互斥鎖
活結 (livelock) 與死結類似,但執行緒不是阻塞等待,而是在迴圈中持續檢查(如自旋鎖 spin lock) 嚴重時表現與死結相同(應用停止回應),但 CPU 使用率居高不下;不嚴重時可靠隨機排程化解 兩執行緒不斷互相禮讓、重試,誰也無法前進
I/O 阻塞 / 外部輸入 執行緒被外部輸入阻塞,即使該輸入永遠不會發生,執行緒也無法做其他事 該執行緒卡住,連帶使等待它完成任務的其他執行緒一起阻塞 等待使用者輸入或網路資料的執行緒,同時肩負其他職責
死結 vs 活結的快速判別

兩者外在表現都可能是「程式卡住」,關鍵區別是 CPU 使用率:死結執行緒阻塞休眠(CPU 低),活結執行緒忙碌空轉(CPU 高)。

競爭條件 (Race Condition)

競爭條件在多執行緒程式中非常普遍——獨立執行緒的排程順序決定了誰先誰後。許多競爭條件是良性的(例如哪個執行緒先取得任務佇列中的下一個任務),但惡性的競爭條件是大量併發 bug 的根源;許多死結與活結的根因其實也是競爭條件。

類型 定義 典型表現 關鍵成因
資料競爭 (data race) 未同步地並發存取同一塊共享記憶體 → 未定義行為 (undefined behavior)(見第5章記憶體模型) 隨機崩潰、讀到不一致的值 錯誤使用原子操作做同步;或根本沒用互斥鎖保護共享資料
破壞不變量 基於資料」的問題:資料處於不變量被破壞的中間狀態時被其他執行緒觀察或操作 懸空指標(資料已被其他執行緒刪除)、隨機記憶體錯誤(局部更新導致讀到不一致資料)、雙重釋放(兩執行緒同時對同一佇列 pop 同一元素) 錯誤的同步導致執行順序被破壞(操作間有順序依賴卻未強制)
生命週期問題 執行緒存取已不存在的資料(被刪除、銷毀,或已轉移到其他物件) 典型:執行緒引用區域變數,而區域變數在執行緒結束前就先「壽終」 執行緒生命週期超過其引用的資料;join() 未涵蓋異常路徑(拋出異常時仍須等待執行緒完成——執行緒的基本異常安全)
生命週期問題可視為破壞不變量的特例

書中將其獨立成類,因為問題焦點不同:不是「資料狀態不一致」,而是「資料根本已經不存在」。修復方向也不同——調整資料/執行緒的生命週期(如 join() 覆蓋所有路徑),而非加鎖。

症狀對照:阻塞型 vs 競爭型

面向 不必要的阻塞 惡性競爭條件
外在表現 應用掛起、反應遲鈍、任務超長時間才完成 隨機崩潰、錯誤輸出、記憶體被覆寫
除錯方式 可用除錯器附掛 (attach) 到死結/活結的執行緒直接觀察 依賴執行時序,問題難以定位到具體位置
可重現性 相對穩定 隨排程改變,結果時對時錯
共享記憶體的詛咒

應用中的任何執行緒都可能覆寫其他執行緒可存取的資料。競爭型 bug 的破壞現場往往離肇因很遠——必須靠限制執行緒可存取的資料範圍、並正確使用同步來預防,而不能只靠事後除錯。


Exam/Test Patterns

情境/關鍵字 答案
「併發 bug 分哪兩大類?」 不必要的阻塞 + 競爭條件
「程式卡住、CPU 使用率極低」 死結(阻塞等待)
「程式卡住、CPU 使用率居高不下」 活結(迴圈持續檢查,如自旋鎖);輕微時可用隨機排程化解
「未同步存取共享記憶體會怎樣?」 資料競爭 → 未定義行為(第5章記憶體模型)
「懸空指標、雙重釋放、局部更新讀到怪資料」 破壞不變量(基於資料的問題)
「執行緒引用區域變數,變數先被銷毀」 生命週期問題;join() 須涵蓋異常路徑
「隨機崩潰、時對時錯、難以重現」 惡性競爭條件的典型症狀(時序相依)
「所有競爭條件都是 bug 嗎?」 否,良性競爭條件很常見(如哪個執行緒取得下一個任務)