隱含式執行緒:Thread Pool、Fork-Join、OpenMP、GCD、TBB (Implicit Threading)
本系列文章內容參考自經典教材 Operating System Concepts, 10th Edition (Silberschatz, Galvin, Gagne)。本文對應章節:Section 4.5 Implicit Threading。
為什麼需要 Implicit Threading?
在前幾節,開發者需要自行呼叫 pthread_create、CreateThread、new Thread(...) 等 API 來明確建立每一條執行緒,並負責管理它的生命週期。這種做法稱為顯含式執行緒(Explicit Threading)。然而,當應用程式的並行度越來越高,顯含式做法面臨兩類困境:
-
並行 正確性極難保證:多執行緒程式中,競爭條件(Race Condition)、死鎖(Deadlock)、資料一致性問題隨著執行緒數量呈指數級複雜化。Chapter 6 和 Chapter 8 會深入討論這些主題,但核心困難在此:手動管理執行緒的開發者需要同時理解演算法邏輯與並行控制機制,兩者交織在一起,極易出錯。
-
效能調校依賴硬體細節:多少執行緒最合適?取決於核心數、記憶體大小、任務特性。這些資訊在編譯期通常未知,開發者必須在執行期動態判斷,且每換一台機器就可能需要重新調整。
解決方向:把執行緒管理的責任交出去。 這個策略稱為隱含式執行緒(Implicit Threading),由編譯器(Compiler) 或執行期函式庫(Run-Time Library) 代替開發者決定如何建立與管理執行緒。開發者只需要識別哪些任務(Tasks) 可以並行執行,而不是直接操作執行緒。任務通常寫成一個函式,由執行期函式庫自動將其映射到實際的執行緒上,通常採用 Many-to-Many 模型(Section 4.3.3)。
本節介紹五種主流的隱含式執行緒方案:Thread Pool、Fork-Join、OpenMP、Grand Central Dispatch(GCD)、Intel Thread Building Blocks(TBB)。
4.5.1 Thread Pool(執行緒池)
問題場景:每個請求建立一條新執行緒
假設有一台多執行緒網頁伺服器,採用最直覺的設計:每收到一個客戶端請求,就建立一條新執行緒來服務這個請求,服務完成後銷毀執行緒。雖然這比「每個請求 fork 一個新 Process」快很多,但這個設計有兩個根本性的問題:
-
執行緒建立成本不可忽視:每次請求都要執行 OS 的執行緒建立流程,包含分配核心資源、初始化 stack 等,這段時間在高並發場景下會累積成顯著的延遲。服務一個請求所需的時間,本身就包含了建立執行緒的時間,而非完全用在實際工作上。
-
執行緒數量沒有上限:如果每個請求都建立一條執行緒,在高流量時可能同時存在數千條執行緒。執行緒本身佔用 CPU time 和記憶體(每條執行緒的 stack 空間),無限制地建立執行緒最終會耗盡系統資源,導致服務崩潰。
Thread Pool(執行緒池) 是解決這兩個問題的標準方案。
Thread Pool 的核心機制
Thread Pool 的核心思想是:在系統啟動時預先建立一批執行緒,讓它們待在「池(Pool)」中等待工作,而不是按需建立、用完銷毀。
以下是一次完整的請求處理流程:
這個流程圖說明:
- 有閒置執行緒:喚醒一條,立刻服務請求,完成後執行緒回到池中等待下一個任務
- 無閒置執行緒:任務進入等待佇列(Task Queue),一旦某條執行緒完成工作、回到池中,立刻從佇列取出下一個任務
執行緒池能正常運作的前提是:提交給池的任務可以非同步執行(Asynchronously),即提交者不需要原地等待任務完成。
Thread Pool 的三大優勢
| 優勢 | 說明 |
|---|---|
| ① 更快的響應速度 | 用既有的閒置執行緒服務請求,比臨時建立新執行緒快,省去 OS 的執行緒建立開銷 |
| ② 限制並行執行緒數量 | 池的大小設定了並行執行緒的上限,防止系統因大量執行緒而耗盡資源;在無法支援大量並行執行緒的系統上尤為重要 |
| ③ 任務與執行緒解耦 | 任務的定義(做什麼)和執行緒的管理(怎麼跑)分離,讓執行策略可以彈性調整,例如:延遲執行、週期執行,而不需要修改任務本身的程式碼 |
池中執行緒的數量可以根據系統 CPU 數量、實體記憶體大小、預期並發請求數等因素以啟發式(Heuristically) 方法決定。進階的 Thread Pool 架構(如後文的 GCD)甚至能根據當前負載動態增減執行緒數量:負載低時縮池以節省記憶體,負載高時擴池以維持吞吐量。
Windows API Thread Pool
Windows API 的 Thread Pool 使用方式與 CreateThread() 相似,但把執行緒管理交給系統。首先定義一個要在獨立執行緒上跑的函式:
DWORD WINAPI PoolFunction(PVOID Param) {
/* this function runs as a separate thread */
}
接著透過 QueueUserWorkItem() 提交給 Thread Pool:
QueueUserWorkItem(&PoolFunction, NULL, 0);
三個參數分別是:
Function(LPTHREAD_START_ROUTINE):要執行的函式指標Param(PVOID):傳給函式的參數Flags(ULONG):控制執行緒池如何建立與管理執行緒
呼叫這行程式碼後,Thread Pool 中的某條執行緒會代替開發者執行 PoolFunction()。開發者不知道是哪條執行緒在執行,也不需要知道,執行緒的選取完全由 Thread Pool 管理。Windows Thread Pool API 還提供其他功能,例如:在非同步 I/O 完成後觸發函式、或按週期性間隔呼叫函式。
Java Thread Pool
Java 的 java.util.concurrent 套件提供多種 Thread Pool 架構,核心介面是 ExecutorService,透過 Executors 工廠類別建立:
| 工廠方法 |
|---|