线程池

**執行緒池**（英語：thread pool）：一種執行緒使用模式。執行緒過多會帶來排程開銷，進而影響快取局部性和整體效能。而執行緒池維護著多個執行緒，等待著監督管理者分配可並行執行的任務。這避免了在處理短時間任務時建立與銷毀執行緒的代價。執行緒池不僅能夠保證核心的充分利用，還能防止過分排程。可用執行緒數量應該取決於可用的並行處理器、處理器核心、記憶體、網路 sockets 等的數量。 例如，對於計算密集型任務，執行緒數一般取 cpu 數量 + 2 比較合適，執行緒數過多會導致額外的執行緒切換開銷。

任務排程以執行執行緒的常見方法是使用同步佇列，稱作任務佇列。池中的執行緒等待佇列中的任務，並把執行完的任務放入完成佇列中。

執行緒池模式一般分為兩種：HS/HA 半同步 / 半非同步模式、L/F 領導者與跟隨者模式。

* 半同步 / 半非同步模式又稱為生產者消費者模式，是比較常見的實現方式，比較簡單。分為同步層、佇列層、非同步層三層。同步層的主執行緒處理工作任務並存入工作佇列，工作執行緒從工作佇列取出任務進行處理，如果工作佇列為空，則取不到任務的工作執行緒進入掛起狀態。由於執行緒間有資料通信，因此不適於大數據量交換的場合。
* 領導者跟隨者模式，線上程池中的執行緒可處在 3 種狀態之一：領導者 leader、追隨者 follower 或工作者 processor。任何時刻執行緒池只有一個領導者執行緒。事件到達時，領導者執行緒負責訊息分離，並從處於追隨者執行緒中選出一個來當繼任領導者，然後將自身設定為工作者狀態去處置該事件。處理完畢後工作者執行緒將自身的狀態置為追隨者。這一模式實現複雜，但避免了執行緒間交換任務資料，提高了 CPU cache 相似性。在 ACE (Adaptive Communication Environment) 中，提供了領導者跟隨者模式實現。

執行緒池的伸縮性對效能有較大的影響。

* 建立太多執行緒，將會浪費一定的資源，有些執行緒未被充分使用。
* 銷毀太多執行緒，將導致之後浪費時間再次建立它們。
* 建立執行緒太慢，將會導致長時間的等待，效能變差。
* 銷毀執行緒太慢，導致其它執行緒資源飢餓。

## Windows API 的執行緒池函式

Windows 作業系統的 API 提供了一套執行緒池的實現介面。可以方便地建立、使用執行緒池。Windows 執行緒池 API 被設計為一組協同物件， 其中有些物件表示工作單位、計時器、非同步 I/O 等等。使用下述使用者模式的物件來管理執行緒池及相關的資料：

* 執行緒池物件（pool object）包含了一組工作執行緒（worker threads）。每個行程可以建立多個執行緒池物件，也可以直接使用行程預設的執行緒池。
* 回呼環境 (Threadpool Callback Environment) 執行緒池可以關聯多個回呼環境，回呼環境可單獨指定線上程池內的排程優先級。
* 清理群（clean-up group）物件關聯於一個回呼環境。用於追溯 (track) 執行緒緩衝池回呼。每次建立執行緒池的 IO、work、timer、wait 等物件（呼叫 CreateThreadpool\* 等函式），會在 cleanup group 中增加一個成員。關閉執行緒池的 IO、work、timer、wait 等物件（呼叫 CloseThreadpool\* 等函式），會在 cleanup group 中刪除相應的成員。
* 執行緒池工作物件（work object） 由一個函式指標和一個被稱為上下文的 void 指標組成，執行緒池每次在執行時將 void 指標送入函式。可非同步多次投寄給執行緒池去執行它的回呼函式。實際上就是以非同步方式呼叫回呼函式。通過 `SubmitThreadpoolWork` 投寄工作物件到任務佇列。如果不建立新的工作物件，則不能更改函式和上下文。
* 執行緒池定時器物件（timer object）在定時器到期時投寄給執行緒池去執行它的回呼函式。 建立 (`CreateThreadpoolTimer`) 並設定 (`SetThreadpoolTimer`) 定時器物件。當定時器物件到期時，工作執行緒會執行定時器物件的回呼函式。
* 執行緒池等待物件（wait object）：在可等待控制代碼（waitable handle，可使用任何核心同步物件的控制代碼）被觸發（signaled）時或逾時到期時，它的回呼函式被投寄給執行緒池去執行。建立 (`CreateThreadpoolWait`) 並設定 (`SetThreadpoolWait`) 等待物件。執行緒池內部的一個執行緒呼叫 WaitForMultipleObjects 並傳入由 SetThreadpoolWait 函式註冊的控制代碼，不斷地組成一個控制代碼組，同時將 Wait\* 函式的 bWaitAll 設為 FALSE，這樣當任何一個控制代碼被觸發，執行緒池就會被喚醒。因 WaitForMultipleObjects 不允許將同一個控制代碼傳入多次，因此必須確保不會用 SetThreadpoolWait 來多次註冊同一個控制代碼，但可以呼叫 DuplicationHandle 複製控制代碼並傳給 Set\* 函式。因 WaitForMultipleObjects 一次最多只能等待 64 個核心物件，因此執行緒池實際上為每 64 個核心物件分配一個執行緒來等待，所以效率比較高。如果要等待超過 64 個以上的核心物件，系統會每 64 個核心物件，就開闢一個執行緒來等待這些核心物件。當執行緒池中一個執行緒呼叫了傳入的回呼函式，對應的等待項將進入「不活躍」狀態；這意味著如果在同一個核心物件被觸發後如果想再次呼叫這個回呼函式，需要呼叫 SetThreadpoolWait 再次註冊；如果傳入的 hObject 為 NULL，將把 pWaitItem 這個等待項從執行緒中移除。
* 執行緒池 I/O 物件把檔案控制代碼關聯到執行緒池的 I/O 完成埠（completion port）。當非同步 I/O 操作完成，一個工作執行緒取得操作的狀態並呼叫 I/O 物件的回呼函式。

相關的 API 函式：

* CreateThreadpool 建立一個執行緒池資料結構
* CloseThreadpool 關閉一個執行緒池
* SetThreadpoolThreadMaximum 設定一個執行緒池的執行緒數量上限。預設執行緒池的最小數量為 1，最大數量為 500
* SetThreadpoolThreadMinimum 設定一個執行緒池的執行緒數量下限
* InitializeThreadpoolEnvironment 初始化一個回呼環境。作用是回呼環境結構體中的將 Version 設為 1，其餘為 0。
* DestroyThreadpoolEnvironment 刪除回呼環境
* SetThreadpoolCallbackPool 把指定的執行緒緩衝池關聯到指定的執行緒池回呼環境。建立執行緒池的 IO、work、timer、wait 等物件會關聯到一個回呼環境上。如果使用系統預設的回呼環境，需要自行負責回呼函式的 DLL 保持裝入狀態，自行管理執行緒池內的各種物件的生存期。執行緒池可以關聯多個回呼環境，回呼環境可單獨指定線上程池內的排程優先級。
* SetThreadpoolCallbackLibrary 指示在回呼函式未執行完的時候其所在的 DLL 庫不能解除安裝
* SetThreadpoolCallbackPriority 設定回呼環境在所屬執行緒池內的優先級
* SetThreadpoolCallbackRunsLong 指示當前回呼函式將執行較長時間。這有助於執行緒池判斷是否建立新執行緒
* CreateThreadpoolCleanupGroup 建立一個 cleanup group 資料結構，關聯到一個回呼環境上
* SetThreadpoolCallbackCleanupGroup 把指定的 cleanup group 關聯到指定的回呼環境。第三個參數設定 CleanupGroupCancelCallback 回呼函式。當呼叫清理組的 CloseThreadpoolCleanupGroupmembers 函式，並為 bCancelPendingCallbacks 傳入 TRUE 來清除清理組時，如果此時尚有未被處理的工作項時，則這個回呼函式被呼叫，這個回掉函式的第一個參數 pvObjectContext 是通過 CreateThreadpool\* 函式傳入的 pvContext，第二個參數 pvCleanupContext 是由 CloseThreadpoolCleanupGroupMembers 的 pvCleanupContext 參數傳入的。
* CloseThreadpoolCleanupGroup 關閉一個 cleanup group 資料結構。cleanup group 不能包含任何成員。
* CloseThreadpoolCleanupGroupMembers 把指定 cleanup group 的所有 work、timer、wait 等物件釋放掉。對於正在執行的回呼函式，會等待其執行完畢。參數 fCancelPendingCallbacks 確定是否尚未開始執行的任務執行。
* CreateThreadpoolWork 建立一個執行緒池工作物件的資料結構
* CloseThreadpoolWork 函式通知執行緒池該物件可釋放。
* SubmitThreadpoolWork 投寄（post）一個工作物件到執行緒池中。一個工作執行緒將會呼叫執行這個工作物件的回呼函式。
* TrySubmitThreadpoolCallback 請求執行緒池一個工作執行緒呼叫指定的回呼函式
* CreateThreadpoolTimer 建立一個執行緒池定時器物件的資料結構
* SetThreadpoolTimer 設定一個執行緒定時器物件的參數。當定時器到期時，這個定時器物件的回呼函式被放入任務佇列等待被一個工作執行緒執行。第三個參數設定周期性到時，不論之前放入任務佇列的回呼函式是否執行完畢，總是把回呼函式再一次放入任務佇列等待被執行。如果不希望一個定時器物件的回呼函式的多個實例並行重疊執行，可以不設定周期性到時參數，而在回呼函式末尾呼叫 SetThreadpoolTimer 重設這個定時器物件，起碼可以簡化除錯。如果呼叫 SetThreadpoolTimer 時當前定時器物件還沒有到期，則會修改該定時器物件的到期時間等屬性。第二個參數 pftDueTime 表示首次到時時間，如果為 NULL，則取消還未到時的定時器的下次到期，不再向任務佇列放入定時器物件的回呼函式，但已經在任務佇列中（正在被執行）的回呼函式仍會被執行。第四個參數是到時的容限上限，涉及定時器匯聚，如計時器 A 會在 5-7 微秒內被觸發，計時器 B 會在 6-8 微秒內。因時間有重疊，所以執行緒池只會喚醒一個執行緒來處理這兩個計時器，以減少用兩個執行緒呼叫時產生的額外的執行緒上下文交換的開銷。
* IsThreadpoolTimerSet 判斷給定的定時器物件當前是否被設定
* CreateThreadpoolWait 建立一個執行緒池等待（wait）物件的資料結構
* SetThreadpoolWait 設定一個執行緒等待物件與一個可等待物件 (waitable object) 的控制代碼繫結並設定逾時時間。當可等待物件的控制代碼被通知 (signaled) 或指定的逾時到期，這個等待物件的回呼函式會被入列（queue）進入任務佇列中並等待被一個工作執行緒執行。如果可等待物件（即函式的第二個參數）的控制代碼被設定為 NULL，（由於可等待物件還沒有 signaled 所以）執行緒池等待物件的回呼函式還未放入任務佇列的都被取消，但已在任務佇列的回呼函式仍會被工作執行緒執行。注意不要多次使用 SetThreadpoolWait 來等待同一個 hObject。第三個參數 pftTimeout 指出執行緒池願意花的最長時間來等待核心物件觸發，0 是立即返回，負值為相對時間，正值為絕對時間，NULL 表示無限等待（執行緒池內部呼叫了 WaitForMultipleObjects）。
* CloseThreadpoolWait 關閉一個執行緒等待物件，然後才能關閉可等待物件 (waitable object) 的控制代碼。
* WaitForThreadpoolWaitCallbacks 等待指定的執行緒池等待物件執行完成或者取消還沒開始執行的執行緒池等待物件。
* WaitForThreadpoolIoCallbacks 等待指定的執行緒池 IO 物件執行完成或者取消還沒開始執行的執行緒池 IO 物件。該函式須在另一個執行緒使用，而不能在回呼函式內部使用，因為這會造成死結。如果 bCancelPendingCallbacks 為 TRUE，那麼當請求完成的時候，回呼函式不會被呼叫（如果尚未被呼叫）。
* WaitForThreadpoolTimerCallbacks 等待指定的執行緒池定時器物件執行完成或者取消還沒開始執行的執行緒池定時器物件。
* WaitForThreadpoolWorkCallbacks 等待指定的執行緒池工作物件執行完成或者取消還沒開始執行的執行緒池工作物件。
* CreateThreadpoolIo 建立一個 IO 完成物件，並繫結一個非同步 IO 的控制代碼。
* CloseThreadpoolIo 釋放 IO 完成物件。應等待所有 IO 操作完成，關閉 IO 檔案控制代碼後再呼叫本函式。解除 IO 物件（工作項）與執行緒池的關聯。
* StartThreadpoolIo 初始化非同步 IO 操作前應先呼叫本函式，否則執行緒池會忽略 IO 操作的完成並記憶體泄露。
* CancelThreadpoolIo 在 IO 失敗或者 IO 直接同步完成時，取消對執行緒池的通知（notification）
* CallbackMayRunLong 回呼函式通知系統需要執行較長時間。只能在呼叫執行緒的回呼函數裡使用。返回 TRUE 時，說明執行緒池還有其他執行緒可供使用。FALSE 則相反。
* DisassociateCurrentThreadFromCallback 表示與回呼函式關聯的工作項在「邏輯上」完成了，執行當前回呼函式的執行緒與初始化當前回呼函式的物件去關聯。這使得所有等待當前回呼函式完成的執行緒被釋放。任何由於呼叫 WaitForThreadpool\*Callbacks（如 WaitForThreadpoolIoCallbacks）而被阻塞的執行緒能早一些返回，而不必等到執行緒從回呼函式中結束時才返回。
* 下述函式與回呼函式實體指標 PTP\_CALLBACK\_INSTANCE 有關，用於在回呼函式內設定當該函式結束時的動作。注意只能有一個動作在回呼函式結束後被執行，不能多次呼叫下述函式，否則最後一次呼叫的函式會覆蓋之前呼叫的下述函式。當執行緒呼叫回呼函式時，Windows 會自動傳一個 pInstance 參數（類型 PTP\_CALLBACK\_INSTANCE) 給回呼函式，然後回呼函式將這個參數又傳給如下的函式，執行一些相應的終止操作：
  * FreeLibraryWhenCallbackReturns 當回呼函式返回的時候，執行緒池會自動呼叫 FreeLibrary，並在參數中傳入指定的 HMOUDLE。如果回呼函式是從 DLL 中載入的，這個函式尤為重要，因為當執行緒執行完畢後，回呼函式不能自己呼叫 FreeLibrary，否則回呼函式代碼將從行程中清除，這樣當 FreeLibrary 試圖返回到回呼函式時，會引發存取違規
  * LeaveCriticalSectionWhenCallbackReturns 當回呼函式返回時，執行緒池會自動呼叫 LeavCriticalSection，並在參數中傳入指定的 CRITCAL\_SECTION 結構體。
  * ReleaseMutexWhenCallbackReturns 當回呼函式返回時，執行緒池會自動呼叫 ReleaseMutex，並在參數中傳入指定的 HANDLE
  * ReleaseSemaphoreWhenCallbackReturns 當回呼函式返回的時候，執行緒池會自動呼叫 ReleaseSemaphore，並在參數中傳入指定的 HANDLE
  * SetEventWhenCallbackReturns 當回呼函式返回的時候，執行緒池會自動呼叫 SetEvent，並在參數中傳入指定的 HANDLE

### 範例

範例程式 1 如下：

關於 IO 執行緒池的一個範例：

## .NET Framework 的執行緒池實現

命名空間 System.Threading 中的類 ThreadPool 提供一個執行緒池，該執行緒池可用於執行任務、傳送工作項、處理非同步 I/O、代表其他執行緒等待以及處理計時器。

## 參看

* 並行
* 並列
* 物件池模式

## 參考文獻