天天看點

[筆記]Windows核心程式設計《十一》Windows線程池前言11.1 情形1:以異步的方式調用函數11.2 情形2:每隔一段時間調用一個函數11.3 情形3:在核心對象觸發時調用一個函數11.4 情形4: 在異步I/O 請求完成時調用一個函數11.5 回調函數的終止操作

參考1

參考2

參考3

文章目錄

  • 前言
  • 11.1 情形1:以異步的方式調用函數
    • 11.1.1 顯式地控制控制
      • 建立一個工作項
      • 向線程池送出一個請求
      • 取消已經送出的工作項或是等待工作項處理完畢
      • 關閉工作項
    • 11.1.2 Batch示例程式
  • 11.2 情形2:每隔一段時間調用一個函數
    • 11.2.1 線程池實作計時器
      • 通知線程池何時調用我們回調
      • 向線程池注冊計時器(或者對計時器進行修改)
      • 确定某個計時器是否已經被設定
      • 等待一個計時器完成
    • 11.2.2 Timed Message Box示例程式
  • 11.3 情形3:在核心對象觸發時調用一個函數
      • 核心對象被觸發時執行某函數
      • 綁定核心對象到線程池
      • 獲得WaitCallback被調用的原因
      • 等待一個等待項完成
      • 釋放一個等待項的記憶體
  • 11.4 情形4: 在異步I/O 請求完成時調用一個函數
      • 定義回調函數
      • 建立一個線程池IO對象
      • IO項中的裝置與IO完成端口關聯
      • 停止線程池調用回調函數
      • 取消裝置與線程池的關聯
      • 讓另一個線程等待一個待處理的IO請求完成
  • 11.5 回調函數的終止操作
    • 11.5.1 對線程池進行定制
      • 建立新的線程池
      • 回調環境pcbe
    • 11.5.2 得體地銷毀線程池:清理組
      • 建立一個清理組
      • 将清理組與一個已經綁定到線程池的TP_CALLBACK_ENVIRON結構關聯
      • 銷毀線程池
      • 釋放清理組

前言

線程池通常含義指 一個固定數量的線程隊列。每當需要一個線程去執行某任務(某段代碼),從隊列中選出一個閑置的線程去執行,當線程執行完某任務後,不會立即銷毀,會回到隊列中,等待執行其他任務。

為了簡化程式員的工作,Windows提供了一個線程池機制來簡化線程的建立、銷毀以及日常管理。這個新線程池可能不适用于所有的情況,但大多數情況下它都能夠滿足我們的需要。

這個線程池能夠幫助我們做一下事情:

  1. 以異步的方式調用一個函數。
  2. 每隔一段時間調用一個函數。
  3. 當核心對象觸發時調用一個函數。
  4. 當異步IO請求完成時調用一個函數。

11.1 情形1:以異步的方式調用函數

為了讓線程池來以異步地方式執行一個函數,我們需要 定義一個具有以下原型地函數:

VOID NTAPI SimpleCallback(
PTP_CALLBACK_INSTANCE pInstance,
PVOID pvContext
);
           

為了讓線程池中的一個線程執行該函數,我們需要向線程池送出一個請求。

BOOL TrySubmitThreadpoolCallback(  
      PTP_SIMPLE_CALLBACK pfnCallback,  
      PVOID pvContext,  
      PTP_CALLBACK_ENVIRON pche);  
           

若調用成功,則傳回true。否則傳回false。

系統會自動為我們的程序建立一個預設的線程池,并讓線程池中的一個線程來調用我們的回調函數。當這個線程處理完一個客戶請求後,不會立即被銷毀,而是會回到線程池,準備好處理隊列中的其它工作項。線程池會不斷重複使用其中的線程。如果線程池檢測到建立另一個線程将能夠更好地為應用程式服務,那它就會這樣做。如線程池檢測到它的線程數量已供過于求,那麼它就會銷毀其中一些。

11.1.1 顯式地控制控制

在某些情況下,如記憶體不足時TrySubmitThreadpoolCallback可能會失敗。每一次調用TrySubmitThreadpoolCallback時,系統會在内部配置設定一個工作項。

如果打算送出大量的工作項,出于性能和記憶體使用方面的考慮,應該手動建立工作項然後多次送出它。

建立一個工作項

VOID CALLBACK WorkCallback(  
        PTP_CALLBACK_INSTANCE Instance,  
        PVOID Context,  
        PTP_WORK Work);

PTP_WORK CreateThreadpoolWork(
		PTP_WORK_CALLBACK pfnWorkHandler,
		PVOID pvContext,
		PTP_CALLBACK_ENVIRON pcbe
);
           

該函數會在使用者模式記憶體中建立一個結構來儲存它的三個參數,并傳回指向該結構的指針。

pfnWorkHandler :函數指針,線程池中的線程最終對工作項處理時,會調用該函數指針指向的函數。

pvContext :傳給回調函數的值。

pcbe : 如果傳給它NULL則表示我們會将工作項添加到預設的線程池中。一般情況下預設的線程池能夠滿足大多數情況下的要求。

向線程池送出一個請求

取消已經送出的工作項或是等待工作項處理完畢

如果我們想取消已經送出的工作項或是等待工作項處理完畢。可以調用以下函數:

VOID WaitForThreadpoolWorkCallbacks(  
      PTP_WORK pWork,  
      BOOL bCancelPendingCallbacks);  
           

此函數将線程挂起,直到工作項處理完畢。

pWork : 指向一個工作項。此工作項可以是CreateThreadpoolWork和SubmitThreadpoolWork來建立和送出的。如果工作項尚未被送出,那麼等待函數立即傳回。

bCancelPendingCallbacks:

TRUE,當指定的工作項尚未被處理,函數将其标記為取消并傳回;當指定的工作項正在被處理,等待處理完成後,再傳回;當同一工作項被送出多次時,隻等待目前正處理的。

FALSE,當指定的工作項被處理完且處理它的線程已經被收回并準備處理下一個工作項時,才傳回;當同一工作項被送出多次時,等待所有均被處理後,傳回。

關閉工作項

當不需要一個工作項時,可以調用CloseThreadpoolWork,并傳入指向該工作項的指針。

11.1.2 Batch示例程式

Batch示例程式,如何使用線程池的工作項。

待。

11.2 情形2:每隔一段時間調用一個函數

有時候應用程式需要在某些時間執行某些任務。Windows提供了可等待計時器對象,它使我們我們可以非常友善的得到一個時間通知。我們可以為每個需要執行基于時間的任務建立一個可等待的計時器對象,但這是不必要的。線程池函數為我們解決了這些事情。

11.2.1 線程池實作計時器

通知線程池何時調用我們回調

為了将一個工作項安排在某個時間執行,我們必須定義一個回調函數。該函數會在某個時刻被調用。回調函數原型為:

VOID CALLBACK TimeoutCallback(  
       PTP_CALLBACK_INSTANCE pInstance,  
       PVOID pvContext,  
       PTP_TIMER pTimer); 
           

然後調用下面的函數來通知線程池應在何時調用我們的函數:

PTP_TIMER CreateThreadpoolTimer(  
       PTP_TIMER_CALLBACK pfnTimerCallback,  
       PVOID pvContext,  
       PTP_CALLBACK_ENVIRON pcbe);  
           

這個函數與前面介紹的CreateThreadpoolWork相似。CreateThreadpoolTimer傳回計時器對象。該計時器對象由CreateThreadpoolTimer函數建立并傳回。

pfnTimerCallback :是一個函數指針。指向前面介紹的回調函數TimeroutCallback。每當線程池調用pfnTimerCallback指向的函數時會将pvContext傳給它,并傳給pTimer一個由CreateThreadpoolTimer傳回的計時器對象指針。

pvContext:為傳給回調函數參數。

pcbe:使用的線程池類型 同上,為NULL則是預設線程池。

向線程池注冊計時器(或者對計時器進行修改)

當我們想要向線程池注冊計時器時,應該調用SetThreadpoolTimer:

VOID SetThreadpoolTimer(  
       PTP_TIMER pTimer,  
       PFILETIME pftDueTime,  
       DWORD msPeriod,  
       DWORD msWindowLength);  
           

pTimer:用來辨別CreateThreadpoolTimer傳回的計時器對象。

pftDueTime:表示第一次調用回調函數是什麼時候。傳入一個負值表示一個相對時間。該時間相對于調用SetThreadpoolTimer的時間。傳入-1表示立即調用。傳入的正值以100ns為機關,從1600年的1月1日開始計算。

msPeriod:表示調用回調函數的時間間隔,傳入0表示隻調用1次。

msWindowLength:用來給回調函數的執行增加一些随機性。這使得回調函數會在目前設定的時間到目前設定的觸發時間加上msWindowLength設定的時間之間觸發。這對于多個計時器來說非常有用。這可以避免多個計時器間的沖突。

确定某個計時器是否已經被設定

等待一個計時器完成

最後我們可以通過WaitForThreadpoolTimerCallbacks來等待一個計時器完成。調用CloseThreadpoolTimer來釋放計時器的記憶體。它們與前面介紹的WaitForThreadpoolWork和CloseThreadpoolWork相似。

11.2.2 Timed Message Box示例程式

待完成。

11.3 情形3:在核心對象觸發時調用一個函數

在實際使用中我們會發現我們會經常的等待一個核心對象被觸發,觸發後等待線程又會進入下一輪循環繼續等待。Windows線程池提供了一些機制可以簡化我們的工作。

核心對象被觸發時執行某函數

如果我們想讓核心對象被觸發時執行某函數。需要進行以下步驟,

首先編寫一個回調函數,它是核心對象被觸發時被調用的函數。需要滿足以下原型:

VOID CALLBACK WaitCallback(
    PTP_CALLBACK_INSTANCE pInstance,
    PVOID Context,
    PTP_WAIT Wait,
    TP_WAIT_RESULT WaitResult);
           

綁定核心對象到線程池

然後建立CreateThreadpoolWait來将一個核心對象綁定到線程池:

VOID SetThreadpoolWait(
   PTP_WAIT pWaitItem,
   HANDLE hObject,
   PFILETIME pftTimeout);
           

pWaitItem:用來辨別CreateTheadpoolWait傳回的對象。

hObject:用來辨別核心對象。當此對象被觸發時,回調函數會被調用。

pftTimeout:用來表示線程池最長應該花多少時間來等待核心對象被觸發。傳入0表示不用等待。傳入負值表示相對時間。傳NULL表示無限長的時間。

獲得WaitCallback被調用的原因

線程池内部會讓一個線程調用WaitForMultipleOBjecs函數。傳入SetThreadpoolWait函數注冊的一組句柄,并傳入false給bWaitAll參數。當任何一個核心對象被觸發時,線程池就會被喚醒。當核心對象被觸發或是超出等待時間時,線程池的某個線程就會調用我們的回調函數(WaitCallback)。

WaitResult :用來表示WaitCallback被調用的原因。它可以是以下值:

  • WAIT_OBJECT_0 逾時之前有對象被觸發。
  • WAIT_TIMEOUT 由于逾時導緻回調函數被觸發。
  • WAIT_ABANDONED_0 如果傳入的核心對象是互斥量且被遺棄。回調函數将收到這個值。

一旦線程池調用了我們的回調函數,對應的等待項将進入不活躍狀态。所謂不活躍狀态:如果想讓回調函數在同一個核心對象被觸發時再次被調用,我們需要調用SetThreadpoolWait來再次注冊。

等待一個等待項完成

最後我們同樣可以等待一個等待項完成。這可以調用:

WaitForThreadpoolWaitCallbacks。

釋放一個等待項的記憶體

CloseThreadpoolWait
           

注意:不要讓回調函數調用WaitForThreadpoolWork并将自己的工作項作為參數傳入,這會導緻死鎖。

11.4 情形4: 在異步I/O 請求完成時調用一個函數

我們在上一篇博文中介紹了如何使用IO完成端口來高效的執行異步IO操作,也介紹了如何建立一個線程池并讓其中的線程等待IO完成端口。

這裡我們将介紹線程池如何管理線程的建立和銷毀。在打開一個關聯起來檔案或裝置時,我們必須現将該裝置與線程池的IO完成端口,然後告訴線程池在異步IO完成時應該調用哪個函數。

定義回調函數

首先我們需要定義回調函數,它需要滿足一下原型

VOID CALLBACK OverlappedCompletionRoutine(  
       PTP_CALLBACK_INSTANCE pInstance,  
       PVOID pvContext,  
       PVOID pOverlapped,  
       ULONG IoResult;  
       ULONG_PTR NumberOfBytesTransferred,  
       PTP_IO  pIo);
           

當一個IO操作完成時此回調函數會被調用并得到一個指向OVERLAPPED結構的指針。這個指針是我們在調用ReadFile或WriteFile來發出I/O請求時(通過pOverlapped參數)傳入的。

IoResult:表示IO異步操作的執行結果。如果IO請求成功,将傳給回調函數NO_ERROR。

NumberOfBytesTransferred :參數傳入已傳輸的位元組數。

pIo :傳入指向線程池IO項的指針。馬上介紹。

pInstance :後面會有介紹。

建立一個線程池IO對象

定義好回調函數後,我們就需要調用CreateThreadpoolIo來建立一個線程池IO對象。

PTP_IO CreateThreadpoolIo(  
       HANDLE hDevice,  
       PTP_WIN32_IO_CALLBACK pfnIoCallback,  
       PVOID pvContext,  
       PTP_CALLBACK_ENVIRON pcbe);  
           

hDevice :是與IO對象相關聯的裝置句柄。

pfnIoCallback :是前面我們介紹的回調函數指針。

pvContext : 當然是傳給回調函數的參數。

IO項中的裝置與IO完成端口關聯

當IO對象建立好之後,我們就可以通過下面的函數來将嵌入在IO項中的裝置與IO完成端口關聯起來。

關聯之後我們就可以調用ReadFile或WriteFile了。此後當異步IO請求完成後,回調函數将會被調用。

注意,在每次調用ReadFile或WriteFile之前,我們必須調用StartThreadpoolIo,否則回調函數不會被調用。

停止線程池調用回調函數

此外我們還可以調用以下函數來停止線程池調用回調函數,此後回調函數将不會被調用:

取消裝置與線程池的關聯

CloseThreadpoolIo将取消裝置與線程池的關聯:

讓另一個線程等待一個待處理的IO請求完成

WaitForThreadpoolIoCallbacks函數讓另一個線程等待一個待處理的IO請求完成。

VOID WaitForThreadpoolIoCallback(  
      PTP_IO pio,  
      BOOL bCancelPendingCallbacks);  
           

如果傳給bCancelPendingCallbacks的值為true,那麼當請求完成時,回調函數不會被調用。

11.5 回調函數的終止操作

線程池提供了一種便利的方法,來描述我們的回調函數傳回後,應該執行的一些操作。回調函數用傳給它的不透明的pInstance來調用以下這些函數:

VOID LeaveCriticalSectionWhenCallbackReturns(
PTP_CALLBACK_INSTANCE pci,
PCRITICAL_SECTION pcs
);

VOID ReleaseMutexWhenCallbackReturns(
PTP_CALLBACK_INSTANCE pci,
HANDLE mut
);

VOID ReleaseSemaphoreWhenCallbackReturns(
PTP_CALLBACK_INSTANCE pci,
HANDLE sem,
DWORD crel
);

VOID SetEventWhenCallbackReturns(
PTP_CALLBACK_INSTANCE pci,
HANDLE evt
);

VOID FreeLibraryWhenCallbackReturns(
PTP_CALLBACK_INSTANCE pci,
HMODULE mod
);
           

11.5.1 對線程池進行定制

在調用CreateThreadpoolWork,CreateThreadpoolTimer,CreateThreadpoolWait或CreateThreadpoolIo時,有機會傳入一個PTP_CALLBACK_ENVIRON參數。如傳NULL,會将工作項添加到程序預設的線程池中。

建立新的線程池

PTP_POOL CreateThreadpool(
// 保留,傳NULL。
PVOID reserved
);

// 設定線程池中線程最大,最小數量
BOOL SetThreadpoolThreadMinimum(
PTP_POOL pThreadPool,
DWORD cthrdMin
);

BOOL SetThreadpoolThreadMaximum(
PTP_POOL pThreadPool,
DWORD cthrdMost
);

// 關閉線程池。
// 線程池中線程結束本次處理後結束。
// 線程池隊列中尚未開始處理的項将被取消。
VOID CloseThreadpool(PTP_POOL pThreadPool);
           

線程池始終保持池中的線程數量至少是最小數量,并允許線程數量增長到指定的最大數量。

回調環境pcbe

一旦我們建立了自己的線程池,并指定了線程的最小數量和最大數量,我們就可初始化一個回調環境,它包含了一些可用于工作項的額外的設定或配置。

線程池回調環境資料結構:

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(
PTP_CALLBACK_ENVIRON pcbe
);

// 清理
VOID DestroyThreadpoolEnvironment(
PTP_CALLBACK_ENVIRON pcbe
);

// 為了将一個工作項添加到線程池的隊列中,回調環境必須标明該工作項應由那個線程池來處理。不調用時,pcbe初始化後的Pool字段為NULL,添加到程序預設的線程池。
VOID SetThreadpoolCallbackPool(
PTP_CALLBACK_ENVIRON pcbe,
PTP_POOL pThreadPool
);

// 確定,隻要線程池中還有待處理的工作項,就将一個特定的DLL一直保持在程序的位址空間中。
VOID SetThreadpoolCallbackLibrary(
PTP_CALLBACK_ENVIRON pcbe,
PVOID mod
);
.
           

11.5.2 得體地銷毀線程池:清理組

線程池可處理大量的隊列項,這些項的來源各不相同。

這使我們很難知道線程池結束處理隊列項的确切時間,但隻有這樣才能得體地将它銷毀。

為了幫助我們對線程池進行得體的清理,線程池提供了清理組。

預設的線程池不會被銷毀。在程序終止時,windows會将其銷毀并負責所有的清理工作。

建立一個清理組

将清理組與一個已經綁定到線程池的TP_CALLBACK_ENVIRON結構關聯

VOID SetThreadpoolCallbackCleanupGroup(
	PTP_CALLBACK_ENVIRON pcbe,
	PTP_CLEANUP_GROUP ptpcg,
	// 清理組被取消時,回調函數會被調用
	PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng
);

VOID CALLBACK CleanupGroupCancelCallback(
	PVOID pvObjectContext,
	PVOID pvCleanupContext
);
           

這個函數在内部會設定PTP_CALLBACK_ENVIRON的CleanupGroup字段和CleanupGroupCancelCallback字段。

當調用CreateThreadpoolWork,CreateThreadpoolTimer,CreateThreadpoolWait,CreateThreadpoolIo時,如最後的的參數,即指向PTP_CALLBACK_ENVIRON結構的指針不為NULL,那麼建立的項會被添加到對應的回調環境的清理組中。

在這些隊列項完成後,如我們調用了CloseThreadpoolWork,CloseThreadpoolTimer,CloseThreadpoolWait和CloseThreadpoolIo,等于是隐式地将對應的項從清理組中移除。

銷毀線程池

// 函數會一直等待,直到線程池的工作組中所有剩餘的項(建立但未關閉的)都已處理完畢。
VOID CloseThreadpoolCleanupGroupMembers(
PTP_CLEANUP_GROUP ptpcg,
// TRUE,在所有目前正在運作的工作項完成後傳回。尚未處理的取消。
// FALSE,等待所有項處理完畢
BOOL bCancelPendingCallbacks,
PVOID pvCleanupContext
);
           

如果傳給bCancelPendingCallbacks值為TRUE,且傳給SetThreadpoolCallbackCleanupGroup的pfng不為NULL。則,對每個被取消的工作項,pfng會被調用。回調函數的pvObjectContext包含每個被取消項的上下文(CreateThreadpoolxxx時設定的)。回調函數的pvCleanupContext是上述調用的最後一個參數。

釋放清理組

VOID WINAPI CloseThreadpoolCleanupGroup(
PTP_CLEANUP_GROUP ptpcg
);

// 銷毀自定義線程池的環境
DestroyThreadpoolEnvironment

// 關閉線程池
CloseThreadpool
           

繼續閱讀