以非同步方式調用函數
為了用線程池來以非同步方式執行一個函數,我們需要定義一個具有以下原型的函數:
VOID CALLBACK SimpleCallback(
[in, out] PTP_CALLBACK_INSTANCE Instance,
[in, out, optional] PVOID Context
);
然後為了讓線程池中的一個線程來執行該函數,我們需要向線程池提交一個請求:
BOOL WINAPI TrySubmitThreadpoolCallback(
__in PTP_SIMPLE_CALLBACK pfns,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
注意,我們從來不用自己調用CreateThread.系統會自動為我們的進城建立一個預設的線程池,並讓線程池中的一個線程來調用我們的回呼函數.此外,當這個線程處理完一個客戶請求後,不會立即銷毀,而是回到線程池,準備好處理隊列中的任何其他工作項目.
每一次調用TrySubmitThreadpoolCallback函數的時候,系統會在內部以我們的名義分配一個工作項目。但是在某些情況下,比如記憶體不足,或者配額限制,TrySubmitThreadpoolCallback調用可能會失敗。在多項操作需要相互協調的時候(比如一個計時器要依靠一個工作項目來取消一個操作的時候),這是不能接受的。
所以,我們必須在設定計時器的時候手動建立一個工作項目:
PTP_WORK WINAPI CreateThreadpoolWork(
__in PTP_WORK_CALLBACK pfnwk,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
參數pfnwk 必須符合下面的函數原型:
VOID CALLBACK WorkCallback(
[in, out] PTP_CALLBACK_INSTANCE Instance,
[in, out, optional] PVOID Context,
[in, out] PTP_WORK Work
);
當我們要向線程池提交一個工作項目的時候,可以調用如下函數:
VOID WINAPI SubmitThreadpoolWork(
__in_out PTP_WORK pwk
);
如果我們有另外一個線程,該線程想要取消已經提交的工作項目,或者該線程由於要等待工作項目處理完畢而要將自己掛起:
VOID WINAPI WaitForThreadpoolWorkCallbacks(
__in_out PTP_WORK pwk,
__in BOOL fCancelPendingCallbacks
);
如果參數fCancelPendingCallbacks 為TRUE,那麼WaitForThreadpoolWorkCallbacks 會試圖取消先前提交的那個工作項目。如果線程池中的線程正在處理那個工作項目,那麼WaitForThreadpoolWorkCallbacks 函數會一直等到該工作項目已經完成後再返回。如果已提交的工作項目尚未被任何線程處理,那麼函數會先將它標記為已取消,然後立即返回。當完成連接埠從隊列中取出該工作項目的時候,線程池知道無需調用回呼函數,這樣該工作項目就不會被執行。
如果參數fCancelPendingCallbacks 為FALSE,那麼WaitForThreadpoolWorkCallbacks 會將調用線程掛起,直到指定工作項目處理完成,而且線程池中處理該工作項目的線程也已經被收回並準備好處理下一個工作項目為止。
說明:
1.如果打算提交大量的工作項目,那麼出於對效能和記憶體使用量的考慮,建立工作項目一次,然後多次提交它會更好。
2.如果用一個PTR_WORK對象提交了多個工作項目,而且傳給WaitForThreadpoolWorkCallbacks
函數中的fCancelPendingCallbacks 為FALSE,那麼WaitForThreadpoolWorkCallbacks
函數會等待線程池處理完所有已提交的工作。如果傳給fCancelPendingCallbacks 為TRUE,那麼只會等待當前正在啟動並執行工作項目完成。
如果不在需要一個工作項目,可以調用如下函數:
VOID WINAPI CloseThreadpoolWait(
__in_out PTP_WAIT pwa
);
每隔一段時間調用一個函數
為了將一個工作項目安排在某個時間執行,我們必須定義一個回呼函數,原型如下:
VOID CALLBACK TimeoutCallback(
TP_CALLBACK_INSTANCE Instance,
PVOID Context,
PTP_TIMER pTimer);
然後調用下面的函數來通知線程池應該在何時調用我們的函數:
PTP_TIMER WINAPI CreateThreadpoolTimer(
__in PTP_TIMER_CALLBACK pfnti,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
當我們想要向線程池註冊計時器的時候,應該調用如下函數:
VOID WINAPI SetThreadpoolTimer(
__in_out PTP_TIMER pti,
__in_opt PFILETIME pftDueTime,
__in DWORD msPeriod,
__in_opt DWORD msWindowLength
);
參數pftDueTime 表示第一次調用回呼函數應該是在什麼時候。我們可以傳一個負值(以微妙為單位)來指定一個相對時間,該時間相對於調用SetThreadpoolTimer 的時間。
-1表示立即開始。
為了指定一個絕對時間,我們應該傳入一個正值,這個值以100納秒為單位。
如果只想讓計時器觸發一次,那麼可以給msPeriod 參數傳0。
如果想讓線程池定期調用我們的回呼函數,那麼應該給msPeriod 參數傳一個正值(表示在再次調用我們的TimerCallback之前需要等待多少毫秒。)
參數msWindowLength 用來給回呼函數的執行時間增加一些隨機性,這使得回呼函數會在當前設定的促發時間,到當前設定的促發時間加上msWindowLength設定的時間之間觸發。
在設定了計時器之後,我們還可以調用SetThreadpoolTimer函數並在pti 參數中傳入先前設定的計時器指標,以此來對已有的計時器進行修改。
我們還可以給pftDueTime 傳遞NULL,這等於是告訴線程池停止調用我們的TimerCallback函數。這不失為一種將計時器暫停但又不必銷毀計時器對象的好方法。
我們可以調用如下函數來確定某個計時器是否已經被設定,既它的pftDueTime 參數值不為NULL。
BOOL WINAPI IsThreadpoolTimerSet(
__in_out PTP_TIMER pti
);
最後,我們還可以調用如下函數:
VOID WINAPI WaitForThreadpoolTimerCallbacks(
__in_out PTP_TIMER pti,
__in BOOL fCancelPendingCallbacks
);
VOID WINAPI CloseThreadpoolTimer(
__in_out PTP_TIMER pti
);
在核心對象觸發時調用一個函數
如果要註冊一個工作項目,讓它在一個核心對象被觸發的時候執行,那麼首先要建立一個回呼函數:
VOID CALLBACK WaitCallback(
[in, out] PTP_CALLBACK_INSTANCE Instance,
[in, out, optional] PVOID Context,
[in, out] PTP_WAIT Wait,
[in] TP_WAIT_RESULT WaitResult
);
然後建立線程池對象:
PTP_WAIT WINAPI CreateThreadpoolWait(
__in PTP_WAIT_CALLBACK pfnwa,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
將一個核心對象綁定到這個線程池:
VOID WINAPI SetThreadpoolWait(
__in_out PTP_WAIT pwa,
__in_opt HANDLE h,
__in_opt PFILETIME pftTimeout
);
pwa 參數用來標識CreateThreadpoolWait返回的對象。
h 參數用來標識某個核心對象。
pftTimeout 參數用來表示線程池最長應該花多少時間來等待核心對象被觸發。傳0表示不用等待,傳負值表示相對時間,傳正值表示絕對時間,傳NULL表示無限長等待。
當核心對象被觸發或者超出等待時間的時候,線程池中的某個線程會調用我們的WaitCallback函數。
注意:
一旦線程池的一個線程調用了我們的回呼函數,對應等待項(wait item)將進入不活躍狀態。“不活躍“意味著如果我們想讓回呼函數再次被調用,那麼我們就必須調用SetThreadpoolWait
函數來再次註冊一個核心對象。
在非同步I/O請求完成時調用一個函數
首先,必須編寫符合以下原型的函數:
VOID CALLBACK OverlappedCompletionRoultine (
PTP_CALLBACK_INSTANCE pInstance,
PVOID pvContext,
PVOID POverlapped,
ULONG IoResult,
ULONG_PTR NumberOfBytesTransferred,
PTP_IO pIo);
當一個I/O操作完成時,這個函數會被調用並得到一個指向OVERLAPPED結構的指標,這個指標式我們在調用ReadFile或WriteFile來發出I/O請求的時候(通過pOverlapped參數)傳入的。
操作結果通過IoResult參數傳入,如果I/O成功,那麼該參數為NO_ERROR。
已傳輸的位元組數通過NumberOfByteTransferred參數傳入。
pIo則是一個指向線程池中I/O項的指標。
建立線程池I/O對象:
PTP_IO WINAPI CreateThreadpoolIo(
__in HANDLE fl,
__in PTP_WIN32_IO_CALLBACK pfnio,
__in_out_opt PVOID pv,
__in_opt PTP_CALLBACK_ENVIRON pcbe
);
參數fl 為我們想要與線程池內部的I/O完成連接埠相關聯的檔案/裝置控制代碼(通過用FILE_FLAG_OVERLAPPED標誌調用CreateFile函數所開啟的)。
當線程池I/O對象建立完畢後,我們通過調用下面的函數來將嵌入在I/O項中的檔案/裝置與線程池內部I/O完成連接埠相關聯:
VOID WINAPI StartThreadpoolIo(
__in_out PTP_IO pio
);
注意:
在調用ReadFile 和 WriteFile之前,我們必須調用StartThreadpoolIo。如果每次在發出I/O請求之前沒有調用StartThreadpoolIo,那麼我們的OverlappedCompletionRoultine回呼函數將不會被調用。
如果在發出I/O請求之後讓線程池停止調用我們的回呼函數:
VOID WINAPI CancelThreadpoolIo(
__in_out PTP_IO pio
);
如果在發出請求的時候,ReadFile 和 WriteFile 調用失敗了,那麼我們仍然必須調用CancelThreadpoolIo。例如,如果這兩個函數的傳回值為FALSE 並且 GetLastError的傳回值為ERROR_IO_PENDING以外的值時。
當對檔案/裝置的使用完成後,我們應該調用CloseHandle來將其關閉,並調用如下函數來解除它與線程池的關聯:
VOID WINAPI CloseThreadpoolIo(
__in_out PTP_IO pio
);
我們還可以調用下面的函數來讓另一個線程等待一個待處理的I/O請求完成:
VOID WINAPI WaitForThreadpoolIoCallbacks(
__in_out PTP_IO pio,
__in BOOL fCancelPendingCallbacks
);
如果傳給參數fCancelPendingCallbacks 的值為TRUE,那麼當請求完成的時候,我們的回呼函數不會被調用。這和調用CancelThreadpoolIo 函數的功能相似。
對線程池進行定製
在調用 CreateThreadpoolWork ,CreateThreadpoolIo 等函數的時候,我們給參數PTP_CALLBACK_ENVIRON
設為NULL,那麼我們會將工作項目添加到進程預設的線程池中,預設的線程池的配置能夠很好的滿足大多應用程式的要求。
但是,有時我們想修改線程池中可運行線程的最小數量和最大數量,這時,我們可以調用下面的函數來建立一個新的線程池:
PTP_POOL WINAPI CreateThreadpool(
PVOID reserved
);
參數reserved 是保留的,因此我們應該傳NULL.
設定線程池中線程的最大數量和最小數量:
VOID WINAPI SetThreadpoolThreadMaximum(
__in_out PTP_POOL ptpp,
__in DWORD cthrdMost
);
BOOL WINAPI SetThreadpoolThreadMinimum(
__in_out PTP_POOL ptpp,
__in DWORD cthrdMic
);
預設線程池的最小數量是1,最大數量為500。
當應用程式不再需要自己定製的線程池時:
VOID WINAPI CloseThreadpool(
__in_out PTP_POOL ptpp
);
線程池中當前正在處理隊列中的項的線程會完成他們的處理並終止。而線程池的隊列中所有尚未開始處理的項將被取消。
一旦我們建立了自己的線程池,並指定了線程的最小數量和最大數量,我們就可以初始化一個回調環境(callback environment),既前面函數中傳入的PTP_CALLBACK_ENVIRON型別參數指向的資料結構。
線程池回調環境的資料結構在winnt.h中定義如下:
typedef struct _TP_CALLBACK_ENVIRON {
TP_VERSION Version;
PTP_POOL Pool;
PTP_CLEANUP_GROUP CleanupGroup;
PTP_CLEANUP_GROUP_CANCEL_CALLBACK CleanupGroupCancelCallback;
PVOID RaceDll;
struct _ACTIVATION_CONTEXT *ActivationContext;
PTP_SIMPLE_CALLBACK FinalizationCallback;
union {
DWORD Flags;
struct {
DWORD LongFunction : 1;
DWORD Private : 31;
} s;
} u;
} TP_CALLBACK_ENVIRON, *PTP_CALLBACK_ENVIRON;
對上面結構進行初始化:
VOID InitializeThreadpoolEnvironment(
__out PTP_CALLBACK_ENVIRON pcbe
);
在我們不需要線程池的回調環境時:
VOID DestroyThreadpoolEnvironment(
__in_out PTP_CALLBACK_ENVIRON pcbe
);
為了將一個工作項目添加到線程池的隊列中,回調環境必須標明該工作項目必須由哪個線程池來處理:
VOID SetThreadpoolCallbackPool(
__in_out PTP_CALLBACK_ENVIRON pcbe,
__in PTP_POOL ptpp
);
如果我們不調用上面的函數,那麼TP_CALLBACK_ENVIRON 的Pool欄位會一直為NULL,當用這個回調環境來添加工作項目的時候,工作項目會被添加進預設的線程池。
調用如下函數來告訴回調環境,工作項目通常需要較長的時間來處理,這樣會使得線程池會更快的建立線程:
VOID SetThreadpoolCallbackRunsLong(
__in_out PTP_CALLBACK_ENVIRON pcbe
);
得體的銷毀線程池:清理組
預設的線程池的生命週期與進程相同,在進程終止的時候,Windows會將其銷毀並負責所有清理工作。
我們要清理的是我們建立的私人的線程池。
首先建立一個清理組:
PTP_CLEANUP_GROUP WINAPI CreateThreadpoolCleanupGroup(void);
然後將這個清理組與一個已經綁定到線程池的TP_CALLBACK_ENVIRON 結構關聯起來
VOID SetThreadpoolCallbackCleanupGroup(
__in_out PTP_CALLBACK_ENVIRON pcbe,
__in PTP_CLEANUP_GROUP ptpcg,
__in_opt PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng
);
如果傳給pfng 參數的值不為NULL,那麼回呼函數必須符合下面的原型:
VOID CALLBACK CleanupGroupCancelCallback(
[in, out, optional] PVOID ObjectContext,
[in, out, optional] PVOID CleanupContext
);
銷毀線程池:
VOID WINAPI CloseThreadpoolCleanupGroupMembers(
__in_out PTP_CLEANUP_GROUP ptpcg,
__in BOOL fCancelPendingCallbacks,
__in_out_opt PVOID pvCleanupContext
);
1.如果fCancelPendingCallbacks 參數為TRUE,並且傳給SetThreadpoolCallbackCleanupGroup函數的參數pfng值是一個CleanupGroupCancelCallback
函數的地址,那麼對於每一個被取消的工作項目,我們的回呼函數都會被調用。
2.如果fCancelPendingCallbacks 參數為TRUE,那麼所有已提交但尚未處理的工作項目直接取消,函數會在所有當前正在啟動並執行工作項目完成之後返回。.
參數pvCleanupContext 會返回包含每個被取消的項的上下文。用於在調用CleanupGroupCancelCallback中傳入CleanupContext
的值。
3.如果fCancelPendingCallbacks 參數為FALSE,那麼在返回之前,線程池會花時間來處理隊列中所有剩餘的項,由於項會全部處理完,因此可以給pvCleanupContext參數傳NULL。
.
當所有的工作項目被取消或者被處理之後,我們調用如下函數來釋放清理組所佔用的資源:
VOID WINAPI CloseThreadpoolCleanupGroup(
__in_out PTP_CLEANUP_GROUP ptpcg
);
最後調用:
VOID DestroyThreadpoolEnvironment(
__in_out PTP_CALLBACK_ENVIRON pcbe
);
VOID WINAPI CloseThreadpool(
__in_out PTP_POOL ptpp
);