為什麼使用並發 (Why Use Concurrency)
Overview Table
| 概念 | 重點 |
|---|---|
| 使用並發的兩大理由 | 分離關注點 (SoC) 與 效能 |
| 分離關注點 | 相關代碼放一起、無關代碼分開;執行緒數取決於概念設計而非核心數 |
| 任務平行 (task parallelism) | 把單一任務拆成多部分並行 → 降低總執行時間 |
| 資料平行 (data parallelism) | 各執行緒對不同資料塊做相同操作 → 提升吞吐量 |
| 易並行 (embarrassingly parallel) | 可隨硬體執行緒數擴展的演算法,「人多力量大」 |
| 不用並發的唯一理由 | 收益比不上成本(啟動開銷、資源、切換、複雜度) |
分離關注點 (Separation of Concerns)
編寫軟體時,把相關的代碼放在一起、無關的代碼分開,程式更容易理解與測試、更少出錯。並發讓「必須同時進行的操作」也能各自劃入獨立的功能區域,不必自行編寫任務切換機制、或在操作中主動呼叫不相關代碼。
經典例子:DVD 播放程式(UI 密集型應用)
- 職責一:讀光碟 → 解碼影音 → 輸出至影音硬體(播放)
- 職責二:接收使用者輸入(暫停、返回選單、退出)
- 單執行緒:播放期間必須定期輪詢使用者輸入 → 「播放」與「UI」代碼被迫耦合在一起
- 多執行緒:一個執行緒處理 UI、一個執行緒播放 DVD;兩者只在必要時互動(如按下「暫停」),需人為關聯
好處:
- 回應性的錯覺:UI 執行緒可立即回應使用者請求(即使實際回應只是「忙碌中」游標或「請稍候」訊息)
- 獨立執行緒也常用於背景持續執行的任務(如桌面搜尋監視檔案系統變化)
- 互動清晰可辨,每個執行緒的邏輯都更簡單
Important
以 SoC 為目的劃分執行緒時,執行緒數量基於概念上的設計,與 CPU 核心數無關。
效能:任務平行 vs 資料平行
晶片製造商不再拉高單核速度,而是走多核設計(單晶片 2、4、16 或更多處理器)。如 Herb Sutter 所言:「免費的午餐結束了 (The Free Lunch Is Over)」——程式不會再自動隨新處理器變快;想利用日益增長的計算能力,就必須設計多任務並發的軟體。
利用並發提升效能的兩種方式:
| 方式 | 做法 | 著重點 | 備註 |
|---|---|---|---|
| 任務平行 (task parallelism) | 把單一任務分成幾部分並行執行 | 降低總執行時間 | 看似直觀、實則複雜:各部分間可能存在依賴;可依「流程」切(執行緒各跑演算法的一部分)或依「資料」切 |
| 資料平行 (data parallelism) | 每個執行緒在不同資料塊上執行相同操作 | 同上(任務平行依資料切分的形式) | 依資料切分的任務平行 |
| 以平行解決更大的問題 | 一次處理 2 個、10 個、20 個檔案而非 1 個 | 提升吞吐量:等量資料耗時不變,但相同時間內處理更多資料 | 也是資料平行的一種應用;可解鎖新功能(如平行處理圖片不同區域 → 支援更高解析度影片) |
- 易並行 (embarrassingly parallel) 演算法:具良好可擴展性——硬體執行緒數增加,平行性隨之增加(「人多力量大」)。
- 演算法中不易並行的部分,可劃分成固定(不可擴展)數量的平行任務。
什麼時候不使用並發
Important
知道何時不用並發,與知道何時使用一樣重要。不使用並發的唯一原因:收益比不上成本。
成本清單:
- 腦力與維護成本:並發代碼常更難理解,編寫、維護成本高;複雜性增加 → 錯誤增多。除非潛在效能增益夠大、或關注點分離夠清晰,足以抵銷額外開發與維護成本,否則勿用。
- 執行緒啟動開銷:OS 需分配核心資源與堆疊空間、把新執行緒加入排程器,都需要時間。若任務完成得很快,任務本身耗時可能遠小於啟動執行緒的時間 → 整體效能比單執行緒還差。
- 執行緒是有限資源:太多執行緒同時執行會耗盡 OS 資源,拖慢整個系統。每個執行緒需要獨立堆疊——32-bit(4GB 位址空間)架構下,若每執行緒 1MB 堆疊(常見預設),4096 條執行緒就用光全部位址空間(代碼、靜態資料、堆積都沒空間);64-bit 沒有這種位址空間限制,但其他資源依然有限。
- Context switch 成本:執行緒越多,OS 需要的上下文切換越多,每次切換都耗時——某個程度後,增加執行緒反而降低整體效能。
情境:客戶端/伺服器 (C/S) 應用在伺服器端「每連線一條執行緒」——少量連線沒問題;連線量大時執行緒過多、耗盡系統資源。此時執行緒池 (thread pool) 可最佳化效能(參見第9章)。
Warning
執行緒池能限制執行緒數量,但不是靈丹妙藥——問題(資源、切換、設計複雜度)不會憑空消失。追求最佳效能時,應依硬體並發能力調整執行執行緒的數量。
Tip
並發和其他最佳化策略一樣:可能大幅提升效能,也讓代碼更複雜、更難理解、更易出錯。因此只對效能關鍵部分並發化;但若收益主要在設計清晰或關注點分離,也值得採用多執行緒。
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 「使用並發的兩大理由」 | 分離關注點 (SoC) 與 效能 |
| 「UI 卡住/播放中要即時回應輸入」 | 用執行緒分離關注點:UI 執行緒 + 工作執行緒 |
| 「SoC 劃分執行緒,數量依據?」 | 概念上的設計,與 CPU 核心數無關 |
| 「把演算法拆成多部分並行」 | 任務平行 |
| 「不同資料塊上做相同操作」 | 資料平行 |
| 「時間不變但處理更多資料」 | 以平行解決更大的問題(吞吐量) |
| 「隨硬體執行緒數擴展的演算法」 | 易並行 (embarrassingly parallel) |
| 「免費的午餐結束了」 | Herb Sutter:單核不再變快,須寫多任務並發軟體 |
| 「任務很短,開執行緒反而更慢」 | 啟動開銷(核心資源、堆疊、排程)大於任務本身 |
| 「32-bit + 1MB 堆疊,最多幾條執行緒」 | 約 4096 條即耗盡 4GB 位址空間 |
| 「大量連線的伺服器,每連線一執行緒」 | 資源耗盡 → 用執行緒池(第9章) |
| 「何時不用並發」 | 收益比不上成本 |
Related Notes
- 01-Concurrency-Fundamentals/01-What-Is-Concurrency
- 01-Concurrency-Fundamentals/03-Cpp-Concurrency-Support-and-First-Program
- 01-Concurrency-Fundamentals/Practice-Concurrency-Fundamentals
- [08-Designing-Concurrent-Code/01-Dividing-Work-Between-Threads|08-Designing-Concurrent-Code/01-Dividing-Work-Between-Threads]
- [09-Advanced-Thread-Management/01-Thread-Pools|09-Advanced-Thread-Management/01-Thread-Pools]