併發相關的錯誤類型 (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)、future 或 I/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 嗎?」 | 否,良性競爭條件很常見(如哪個執行緒取得下一個任務) |
Related Notes
- 11-Testing-and-Debugging/02-Locating-and-Testing-Concurrency-Bugs
- 11-Testing-and-Debugging/Practice-Testing-and-Debugging
- 03-Sharing-Data/01-Race-Conditions-and-Mutexes
- 03-Sharing-Data/02-Deadlock-and-Lock-Management
- 05-Memory-Model-and-Atomics/01-Memory-Model-Basics
- 02-Thread-Management/01-Launching-and-Joining-Threads