Windows核心编程 第十一章 线程池的使用
第11章?線程池的使用
? ? 第8章講述了如何使用讓線程保持用戶方式的機制來實現線程同步的方法。用戶方式的同步機制的出色之處在于它的同步速度很快。如果關心線程的運行速度,那么應該了解一下用戶方式的同步機制是否適用。
? ? 到目前為止,已經知道創建多線程應用程序是非常困難的。需要會面臨兩個大問題。一個是要對線程的創建和撤消進行管理,另一個是要對線程對資源的訪問實施同步。為了對資源訪問實施同步,Wi n d o w s提供了許多基本要素來幫助進行操作,如事件、信標、互斥對象和關鍵代碼段等。這些基本要素的使用都非常方便。為了使操作變得更加方便,唯一的方法是讓系統能夠自動保護共享資源。不幸的是,在 Wi n d o w s提供一種讓人滿意的保護方法之前,我們已經有了一種這樣的方法。
? ? M i c r o s o f t公司的Windows 2000提供了一些新的線程池函數,使得線程的創建、撤消和基本管理變得更加容易。這個新的通用線程池并不完全適合每一種環境,但是它常??梢赃m合你的需要,并且能夠節省大量的程序開發時間。
? 新的線程池函數使你能夠執行下列操作:
? ? 異步調用函數。
? ? 按照規定的時間間隔調用函數。
? ? 當單個內核對象變為已通知狀態時調用函數。
? ? 當異步I / O請求完成時調用函數。
11.1 方案1:異步調用函數
? ? ?假設有一個服務器進程,該進程有一個主線程,正在等待客戶機的請求。當主線程收到該請求時,它就產生一個專門的線程,以便處理該請求。這使得應用程序的主線程循環運行,并等待另一個客戶機的請求。這個方案是客戶機 /服務器應用程序的典型實現方法。雖然它的實現方法非常明確,但是也可以使用新線程池函數來實現它。
???當服務器進程的主線程收到客戶機的請求時,它可以調用下面這個函數:
????BOOL QueueUserWorkItem(
????PTHREAD_START_ROUTINEpfnCallback,
????PVOID pvContext,
????ULONG dwFlags
);
? ? 該函數將一個“工作項目”排隊放入線程池中的一個線程中并且立即返回。所謂工作項目是指一個(用 p f n C a l l b a c k參數標識的)函數,它被調用并傳遞單個參數 p v C o n t e x t。最后,線程池中的某個線程將處理該工作項目,導致函數被調用。所編的回調函數必須采用下面的原型:
? ? DWORD WINAPI WorkItemFunc(PVOID pvContext);
? ? 盡管必須使這個函數的原型返回D W O R D,但是它的返回值實際上被忽略了。
注意,你自己從來不調用C r e a t e T h r e a d。系統會自動為你的進程創建一個線程池,線程池中的一個線程將調用你的函數。另外,當該線程處理完客戶機的請求之后,該線程并不立即被撤消。它要返回線程池,這樣它就可以準備處理已經排隊的任何其他工作項目。你的應用程序的運行效率可能會變得更高,因為不必為每個客戶機請求創建和撤消線程。另外,由于線程與完成端口相關聯,因此可以同時運行的線程數量限制為 C P U數量的兩倍。這就減少了線程的上下文轉移的開銷。
? ? 該函數的內部運行情況是,Q u e u e U s e r Wo r k I t e m檢查非I / O組件中的線程數量,然后根據負荷量(已排隊的工作項目的數量)將另一個線程添加給該組件。接著 Q u e u e U s e r Wo r k I t e m執行對P o s t Q u e u e d C o m p l e t i o n S t a t u s的等價調用,將工作項目的信息傳遞給 I / O完成端口。最后,在完成端口上等待的線程取出信息(通過調用 G e t Q u e u e d C o m p l e t i o n S t a t u s) ,并調用函數。當函數返回時,該線程再次調用G e t Q u e u e d C o m p l e t i o n S t a t u s,以便等待另一個工作項目。
? ? 線程池希望經常處理異步 I / O請求,即每當線程將一個 I / O請求排隊放入設備驅動程序時,便要處理異步I / O請求。當設備驅動程序執行該I / O時,請求排隊的線程并沒有中斷運行,而是繼續執行其他指令。異步 I / O是創建高性能可伸縮的應用程序的秘訣,因為它允許單個線程處理來自不同客戶機的請求。該線程不必順序處理這些請求,也不必在等待 I / O請求運行結束時中斷運行。
? ? 但是,Wi n d o w s對異步I / O請求規定了一個限制,即如果線程將一個異步 I / O請求發送給設備驅動程序,然后終止運行,那么該 I / O請求就會丟失,并且在I / O請求運行結束時,沒有線程得到這個通知。在設計良好的線程池中,線程的數量可以根據客戶機的需要而增減。因此,如果線程發出一個異步 I / O請求,然后因為線程池縮小而終止運行,那么該 I / O請求也會被撤消。因為這種情況實際上并不是你想要的,所以你需要一個解決方案。
? ? 如果你想要給發出異步I / O請求的工作項目排隊,不能將該工作項目插入線程池的非 I / O組件中。必須將該工作項目放入線程池的 I / O組件中進行排隊。該I / O組件由一組線程組成,如果這組線程還有尚未處理的 I / O請求,那么它們決不能終止運行。因此你只能將它們用來運行發出異步I / O請求的代碼。
? ? 若要為I / O組件的工作項目進行排隊,仍然必須調用Q u e u e U s e r Wo r k I t e m函數,但是可以為d w F l a g s參數傳遞W T _ E X E C U T E I N I O T H R E A D。通常只需傳遞W T _ E X E C U T E D E FA U LT(定義為0) ,這使得工作項目可以放入非I / O組件的線程中。
? ? Wi n d o w s提供的函數(如R e g N o t i f y C h a n g e K e y Va l u e)能夠異步執行與非 I / O相關的任務。這些函數也要求調用線程不能終止運行。如果想使用永久線程池的線程來調用這些函數中的一個,可以使用W T _ E X E C U T E I N P E R S I S T E N T T H R E A D標志,它使定時器組件的線程能夠執行已排隊的工作項目回調函數。由于定時器組件的線程決不會終止運行,因此可以確保最終發生異步操作。應該保證回調函數不會中斷,并且保證它能迅速執行,這樣,定時器組件的線程就不會受到不利的影響。
? ? 設計良好的線程池也必須設法保證線程始終都能處理各個請求。如果線程池包含 4個線程,并且有1 0 0個工作項目已經排隊,每次只能處理4個工作項目。如果一個工作項目只需要幾個毫秒來運行,那么這是不成問題的。但是,如果工作項目需要運行長得多的時間,那么將無法及時處理這些請求。
? ? 當然,系統無法很好地預料工作項目函數將要進行什么操作,但是,如果知道工作項目需要花費很長的時間來運行,那么可以調用 Q u e u e U s e r Wo r k I t e m函數,為它傳遞W T _ E X E C U T E L O N G F U N C T I O N標志。該標志能夠幫助線程池決定是否要將新線程添加給線程池。如果線程池中的所有線程都處于繁忙狀態,它就會強制線程池創建一個新線程。因此,如果同時對10 000個工作項目進行了排隊(使用W T _ E X E C U T E L O N G F U N C T I O N標志) ,那么這10 000個線程就被添加給該線程池。如果不想創建 10 000個線程,必須分開調用Q u e u e U s e r Wo r k I t e m函數,這樣某些工作項目就有機會完成運行。
? ? 線程池不能對線程池中的線程數量規定一個上限,否則就會發生渴求或死鎖現象。假如有1 00 0 0個排隊的工作項目,當第10 001個項目通知一個事件時,這些工作項目將全部中斷運行。如果你已經設置的最大數量為10 000個線程,第10 001個工作項目沒有被執行,那么所有的10 000個線程將永遠被中斷運行。
? ? 當使用線程池函數時,應該查找潛在的死鎖條件。當然,如果工作項目函數在關鍵代碼段、信標和互斥對象上中斷運行,那么必須十分小心,因為這更有可能產生死鎖現象。始終都應該了解哪個組件(I / O、非I / O、等待或定時器等)的線程正在運行你的代碼。另外,如果工作項目函數位于可能被動態卸載的D L L中,也要小心。調用已卸載的D L L中的函數的線程將會產生違規訪問。若要確保不卸載帶有已經排隊的工作項目的 D L L,必須對已排隊工作項目進行引用計數,在調用Q u e u e U s e r Wo r k I t e m函數之前遞增計數器的值,當工作項目函數完成運行時則遞減該計數器的值。只有當引用計數降為0時,才能安全地卸載D L L。
11.2 方案2:按規定的時間間隔調用函數
? ? 有時應用程序需要在某些時間執行操作任務。 Wi n d o w s提供了一個等待定時器內核對象,因此可以方便地獲得基于時間的通知。許多程序員為應用程序執行的每個基于時間的操作任務創建了一個等待定時器對象,但是這是不必要的,會浪費系統資源。相反,可以創建一個等待定時器,將它設置為下一個預定運行的時間,然后為下一個時間重置定時器,如此類推。然而,要編寫這樣的代碼非常困難,不過可以讓新線程池函數對此進行管理。
? ? 若要調度在某個時間運行的工作項目,首先要調用下面的函數,創建一個定時器隊列:
? ? HANDLE CreateTimeQueue();
? ? 定時器隊列對一組定時器進行組織安排。例如,有一個可執行文件控制著若干個服務程序。每個服務程序需要觸發定時器,以幫助保持它的狀態,比如客戶機何時不再作出響應,何時收集和更新某些統計信息等。讓每個服務程序占用一個等待定時器和專用線程,這是不經濟的。相反,每個服務程序可以擁有它自己的定時器隊列(這是個輕便的資源) ,并且共享定時器組件的線程和等待定時器對象。當一個服務程序終止運行時,它只需要刪除它的定時器隊列即可,因為這會刪除該隊列創建的所有定時器。
? ? 一旦擁有一個定時器隊列,就可以在該隊列中創建下面的定時器:
?
????對于第二個參數,可以傳遞想要在其中創建定時器的定時器隊列的句柄。如果只是創建少數幾個定時器,只需要為h Ti m e r Q u e u e參數傳遞N U L L,并且完全避免調用C r e a t e Ti m e r Q u e u e函數。傳遞N U L L,會告訴該函數使用默認的定時器隊列,并且簡化了你的代碼。 p f n C a l l b a c k和p v C o n t e x t參數用于指明應該調用什么函數以及到了規定的時間應該將什么傳遞給該函數。
? ? d w D u e Ti m e參數用于指明應該經過多少毫秒才能第一次調用該函數(如果這個值是 0,那么只要可能,就調用該函數,使得 C r e a t e Ti m e r Q u e u e Ti m e r函數類似 Q u e u e U s e r Wo r k I t e m) 。d w P e r i o d參數用于指明 應該經過多少毫秒才能在將來調用該函數。如果為 d w P e r i o d傳遞0,那么就使它成為一個單步定時器,使工作項目只能進行一次排隊。新定時器的句柄通過函數的p h N e w Ti m e r參數返回。
? ? 工作回調函數必須采用下面的原型:
VOID WINAPI WaitOrTimerCallback(
???PVOID pvContext,
???BOOL fTimeOrWaitFired);
? ??當該函數被調用時,f Ti m e r O r Wa i t F i r e d參數總是T R U E,表示該定時器已經觸發。下面介紹C r e a t e Ti m e r Q u e u e Ti m e r的d w F l a g s參數。該參數負責告訴函數,當到了規定的時間時,如何給工作項目進行排隊。如果想要讓非 I / O組件的線程來處理工作項目,可以使用W T _ E X E C U T E D E FA U LT。如果想要在某個時間發出一個異步 I / O請求,可以使用W T _ E X E C U T E I N I O T H R E A D。如果想要讓一個決不會終止運行的線程來處理該工作項目,可以使用W T _ E X E C U T E P E R S I S T E N T T H R E A D。如果認為工作項目需要很長的時間來運行,可以使用W T _ E X E C U T E L O N G F U N C T I O N。
? ? 也可以使用另一個標志,即W T _ E X E C U T E I N T I M E RT H R E A D,下面將介紹它。在表11 - 1中,能夠看到線程池有一個定時器組件。該組件能夠創建單個定時器內核對象,并且能夠管理它的到期時間。該組件總是由單個線程組成。當調用 C r e a t e Ti m e r Q u e u e Ti m e r函數時,可以使定時器組件的線程醒來,將你的定時器添加給一個定時器隊列,并重置等待定時器內核對象。然后該定時器組件的線程便進入待命睡眠狀態,等待該等待定時器將一個 A P C放入它的隊列。當等待定時器將該A P C放入隊列后,線程就醒來,更新定時器隊列,重置等待定時器,然后決定對現在應該運行的工作項目執行什么操作。
? ? 接著,該線程要檢查下面這些標志: W T _ E X E C U T E D E FA U LT、W T _ E X E C U T E I N I O T H R E A D、W T _ E X E C U T E I N P E R S I S T E N T T H R E A D、W T _ E X E C U T E L O N G F U N C T I O N和W T _E X E C U T E I N T I M E RT H R E A D。不過現在可以清楚地看到 W T _ E X E C U T E D I N T I M E RT H R E A D標志執行的是什么操作:它使 定時器組件的線程能夠執行該工作項目。雖然這使工作項目的運行效率更高,但是這非常危險。如果工作項目函數長時間中斷運行,那么等待定時器的線程就無法執行任何其他操作。雖然等待定時器可能仍然將 A P C項目排隊放入該線程,但是在當前運行的函數返回之前,這些工作項目不會得到處理。如果打算使用定時器線程來執行代碼,那么該代碼應該迅速執行,不應該中斷。
W T _ E X E C U T E I N I O T H R E A D、W T _ E X E C U T E I N P E R S I S T E N T T H R E A D和W T _ E X E C U T E I N T I M E RT H R E A D等標志是互斥的。如果不傳遞這些標志中的任何一個(或者使用W T _ E X E C U T E D E FA U LT標志) ,那么工作項目就排隊放入I / O組件的線程中。另外,如果設定了W T _ E X E C U T E I N T I M E RT H R E A D標志,那么W T _ E X E C U T E L O N G F U N C T I O N將被忽略。
? ? 當不再想要觸發定時器時,必須通過調用下面的函數將它刪除:
BOOL DeleteTimerQueueTime(
??HANDLE hTimeQueue,
??HANDLE hTimer,
??HANDLE hCompletionEvent);
? ? 即使對于已經觸發的單步定時器,也必須調用該函數。 h Ti m e r Q u e u e參數指明定時器位于哪個隊列中。h Ti m e r參數指明要刪除的定時器,句柄通過較早時調用C r e a t e Ti m e r Q u e u e Ti m e r來返回。
另外,如果你正在使用定時器組件的線程,不應該試圖對任何定時器進行中斷刪除,否則就會產生死鎖。如果試圖刪除一個定時器,就會將一個 A P C通知放入該定時器組件的線程隊列中。如果該線程正在等待一個定時器被刪除,而它不能刪除該定時器,那么就會發生死鎖。
一旦創建了一個定時器,可以調用下面這個函數來改變它的到期時間和到期周期:
?
當不再需要一組定時器時,可以調用下面這個函數,刪除定時器隊列:
?
? ? 該函數取出一個現有的定時器隊列的句柄,并刪除它里面的所有定時器,這樣就不必為刪除每個定時器而顯式調用D e l e t e Ti m e r Q u e u e Ti m e r。h C o m p l e t i o n E v e n t參數在這里的語義與它在D e l e t e Ti m e r Q u e u e Ti m e r函數中的語義是相同的。這意味著它存在同樣的死鎖可能性,因此必須小心。
11.3 方案3:當單個內核對象變為已通知狀態時調用函數
? ? M i c r o s o f t發現,許多應用程序產生的線程只是為了等待內核對象變為已通知狀態。一旦對象得到通知,該線程就將某種通知移植到另一個線程,然后返回,等待該對象再次被通知。有些編程人員甚至編寫了代碼,在這種代碼中,若干個線程各自等待一個對象。這對系統資源是個很大的浪費。當然,與創建進程相比,創建線程需要的的開銷要小得多,但是線程是需要資源的。每個線程有一個堆棧,并且需要大量的 C P U指令來創建和撤消線程。始終都應該盡量減少它使用的資源。
? ? 如果想在內核對象得到通知時注冊一個要執行的工作項目,可以使用另一個新的線程池函數:
?
? ? 該函數負責將參數傳送給線程池的等待組件。你告訴該組件,當內核對象(用 h O b j e c t進行標識)得到通知時,你想要對工作項目進行排隊。也可以傳遞一個超時值,這樣,即使內核對象沒有變為已通知狀態,也可以在規定的某個時間內對工作項目進行排隊。超時值 0和I N F I N I T E是合法的。一般來說,該函數的運行情況與 Wa i t F o r S i n g l e O b j e c t函數(第9章已經介紹)相似。當注冊了一個等待組件后,該函數返回一個句柄(通過 p h N e w Wa i t O b j e c t參數)以標識該等待組件。
? ? 在內部,等待組件使用Wa i t F o r M u l t i p l e O b j e c t s函數來等待已經注冊的對象,并且要受到該函數已經存在的任何限制的約束。限制之一是它不能多次等待單個句柄。因此,如果想要多次注冊單個對象,必須調用D u p l i c a t e H a n d l e函數,并對原始句柄和復制的句柄分開進行注冊。當然,Wa i t F o r M u l t i p l e O b j e c t s能夠等待已通知的對象中的任何一個,而不是所有的對象。如果熟悉Wa i t F o r M u l t i p l e O b j e c t s函數,那么一定知道它一次最多能夠等待 6 4(M A X I M U M _WA I T _ O B J E C T S)個對象。如果用R e g i s t e r Wa i t F o r S i n g l e O b j e c t函數注冊的對象超過6 4個,那么將會出現什么情況呢?這時等待組件就會添加另一個也調用 Wa i t F o r M u l t i p l e O b j e c t s函數的線程。實際上,每隔6 3個對象后,就要將另一個線程添加給該組件,因為這些線程也必須等待負責控制超時的等待定時器對象。
? ? 當工作項目準備執行時,它被默認排隊放入非 I / O組件的線程中。這些線程之一最終將會醒來,并且調用你的函數,該函數的原型必須是下面的形式:
VOID WINAPI WaitOrTimerCallbackFunc(
????PVOID pvContext,
????BOOLEAN fTimerOrWaitFired
);
? ? 如果等待超時了,f Ti m e r O r Wa i t F i r e d參數的值是T R U E。如果等待時對象變為已通知狀態,則該參數是FA L S E。
? ? 對于R e g i s t e r Wa i t F o r S i n g l e O b j e c t函數的d w F l a g s參數,可以傳遞 W T _ E X E C U T E I N -WA I T T H R E A D,它使等待組件的線程之一運行工作項目函數本身。它的運行速率更高,因為工作項目不必排隊放入 I / O組件中。但是這樣做有一定的危險性,因為正在執行工作項目的等待組件函數的線程無法等待其他對象得到通知。只有當工作項目函數運行得很快時,才應該使用該標志。
? ? 如果工作項目將要發出異步 I / O請求,或者使用從不終止運行的線程來執行某些操作,那么也可以傳遞W T _ E X E C U T E I N I O T H R E A D或者W T _ E X E C U T E I N P E R S I S T E N T T H R E A D。也可以使用W T _ E X E C U T E L O N G F U N C T I O N標志來告訴線程池,你的函數可能要花費較長的時間來運行,而且它應該考慮將一個新線程添加給線程池。只有當工作項目正在被移植到非 I / O組件或I / O組件中時,才能使用該標志,如果使用等待組件的線程,不應該運行長函數。
? ? 應該了解的最后一個標志是W T _ E X E C U T E O N LY O N C E。假如你注冊一個等待進程內核對象的組件,一旦該進程對象變為已通知狀態,它就停留在這個狀態中。這會導致等待組件連續地給工作項目排隊。對于進程對象來說,可能不需要這個行為特性。如果使用 W T _E X E C U T E O N LY O N C E標志,就可以防止出現這種情況,該標志將告訴等待組件在工作項目執行了一次后就停止等待該對象。
? ? 現在,如果正在等待一個自動重置的事件內核對象。一旦該對象變為已通知狀態,該對象就重置為它的未通知狀態,并且它的工作項目將被放入隊列。這時,該對象仍然處于注冊狀態,同時,等待組件再次等待該對象被通知,或者等待超時(它已經重置)結束。當不再想讓該等待組件等待你的注冊對象時,必須取消它的注冊狀態。即使是使用 W T _ E X E C U T E O N LY O N C E標志注冊的并且已經擁有隊列的工作項目的等待組件,情況也是如此。調用下面這個函數,可以取消等待組件的注冊狀態:
BOOL UnregisterWaitEx(
????HANDLE hWaitHandle,
????HANDLE hCompIetionEvent
);
? ? 第一個參數指明一個注冊的等待(由 R e g i s t e r Wa i t F o r S i n g l e O b j e c t返回) ,第二個參數指明當已注冊的、正在等待的所有已排隊的工作項目已經執行時,你希望如何通知你。與D e l e t e Ti m e r Q u e u e Ti m e r函數一樣,可以傳遞 N U L L(如果不要通知的話) ,或者傳遞I N VA L I D _ H A N D L E _ VA L U E(中斷對函數的調用,直到所有排隊的工作項目都已執行) ,也可以傳遞一個事件對象的句柄(當排隊的工作項目已經執行時,它就會得到通知) 。對于無中斷的函數調用,如果沒有排隊的工作項目,那么U n r e g i s t e r Wa i t E x返回T R U E,否則它返回FA L S E,而G e t L a s t E r r o r返回S TAT U S _ P E N D I N G。
? ? 同樣,當你將I N VA L I D _ H A N D L E _ VA L U E傳遞給U n r e g i s t e r Wa i t E x時,必須小心避免死鎖狀態。在試圖取消等待組件的注冊狀態,從而導致工作項目運行時,該工作項目函數不應該中斷自己的運行。這好像是說:暫停我的運行,直到我完成運行為止一樣——這會導致死鎖。然而,如果等待組件的線程運行一個工作項目,而該工作項目取消了導致工作項目運行的等待組件的注冊狀態,U n r e g i s t e r Wa i t E x是可以用來避免死鎖的。還有一點需要說明,在取消等待組件的注冊狀態之前,不要關閉內核對象的句柄。這會使句柄無效,同時,等待組件的線程會在內部調用Wa i t F o r M u l t i p l e O b j e c t s函數,傳遞一個無效句柄。Wa i t F o r M u l t i p l e O b j e c t s的運行總是會立即失敗,整個等待組件將無法正常工作。
? ? 最后,不應該調用P u l s e E v e n t函數來通知注冊的事件對象。如果這樣做了,等待組件的線程就可能忙于執行某些別的操作,從而錯過了事件的觸發。這不應該是個新問題了。P u l s e E v e n t幾乎能夠避免所有的線程結構產生這個問題。
11.4 方案4:當異步I / O請求完成運行時調用函數
? ? 最后一個方案是個常用的方案,即服務器應用程序發出某些異步 I / O請求,當這些請求完成時,需要讓一個線程池準備好來處理已完成的 I / O請求。這個結構是I / O完成端口原先設計時所針對的一種結構。如果要管理自己的線程池,就要創建一個 I / O完成端口,并創建一個等待該端口的線程池。還需要打開多個 I / O設備,將它們的句柄與完成端口關聯起來。當異步 I / O請求完成時,設備驅動程序就將“工作項目”排隊列入該完成端口。
? ? 這是一種非常出色的結構,它使少數線程能夠有效地處理若干個工作項目,同時它又是一種很特殊的結構,因為線程池函數內置了這個結構,使你可以節省大量的設計和精力。若要利用這個結構,只需要打開設備,將它與線程池的非 I / O組件關聯起來。記住,I / O組件的線程全部在一個I / O組件端口上等待。若要將一個設備與該組件關聯起來,可以調用下面的函數:
?
? ? 該函數在內部調用 C r e a t e I o C o m p l e t i o n P o r t,傳遞h D e v i c e和內部完成端口的句柄。調用B i n d I o C o m p l e t i o n C a l l b a c k也可以保證至少有一個線程始終在非 I / O組件中。與該設備相關聯的完成關鍵字是重疊完成例程的地址。這樣,當該設備的 I / O運行完成時,非I / O組件就知道要調用哪個函數,以便它能夠處理已完成的I / O請求。該完成例程必須采用下面的原型:
VOID WAINAPI OverlappedCompletionRoutine(
????DWORD dwErrorCode,
????DWORD dwNumberOfBytesTransferred,
????POVERLAPPED pOverlapped
);
? ? 你將會注意到沒有將一個 O V E R L A P P E D結構傳遞給 B i n d I o C o m p l e t i o n C a l l b a c k。O V E R L A P P E D結構被傳遞給R e a d F i l e和Wr i t e F i l e之類的函數。系統在內部始終保持對這個帶有待處理I / O請求的重疊結構進行跟蹤。當該請求完成時,系統將該結構的地址放入完成端口,從而使它能夠被傳遞給你的O v e r l a p p e d C o m p l e t i o n R o u t i n e函數。另外,由于該完成例程的地址是完成的關鍵,因此,如果要將更多的上下文信息放入 O v e r l a p p e d C o m p l e t i o n R o u t i n e函數,應該使用將上下文信息放入O V E R L A P P E D結構的結尾處的傳統方法。
? ? 還應該知道,關閉設備會導致它的所有待處理的 I / O請求立即完成,并產生一個錯誤代碼。要作好準備,在你的回調函數中處理這種情況。如果關閉設備后你想確保沒有運行任何回調函數,那么必須引用應用程序中的計數特性。換句話說,每次發出一個 I / O請求時,必須使計數器的計數遞增,每次完成一個I / O請求,則遞減計數器的計數。
? ? 目前沒有特殊的標志可以傳遞給B i n d I o C o m p l e t i o n C a l l b a c k函數的d w F l a g s參數,因此必須傳遞0。相信你能夠傳遞的標志是 W T _ E X E C U T E I N I O T H R E A D。如果一個I / O請求已經完成,它將被排隊放入一個非I / O組件線程。在O v e r l a p p e d C o m p l e t i o n R o u t i n e函數中,可以發出另一個異步I / O請求。但是記住,如果發出I / O請求的線程終止運行,該I / O請求也會被撤消。另外,非I / O組件中的線程是根據工作量來創建或撤消的。如果工作量很小,該組件中的線程就會終止運行,其 I / O請求仍然處于未處理狀態。如果 B i n d I o C o m p l e t i o n C a l l b a c k函數支持W T _ E X E C U T E I N I O T H R E A D標志,那么在完成端口上等待的線程就會醒來,并將結果移植到一個I / O組件的線程中。由于在I / O請求處于未處理狀態下時這些線程決不會終止運行,因此可以發出I / O請求而不必擔心它們被撤消。
? 雖然W T _ E X E C U T E I N I O T H R E A D標志的作用不錯,但是可以很容易模仿剛才介紹的行為特性。在 O v e r l a p p e d C o m p l e t i o n R o u t i n e函數中,只需要調用 Q u e u e U s e r Wo r k I t e m,傳遞W T _ E X E C U T E I N I O T H R E A D標志和想要的任何數據(至少是重疊結構) 。這就是線程池函數能夠為你執行的全部功能。
?
總結:線程池相關:
1.異步調用函數
2.按規定的時間間隔調用函數
3.當單個內核對象變為已通知狀態時調用函數
4.當異步I / O請求完成運行時調用函數
?
?
?
總結
以上是生活随笔為你收集整理的Windows核心编程 第十一章 线程池的使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows核心编程 第九章 线程与内
- 下一篇: Windows核心编程 第十二章 纤程