到目前為止,已經知道建立多線程應用程式是非常困難的。需要會面臨兩個大問題。一個是要對線程的建立和撤消進行管理,另一個是要對線程對資源的通路實施同步。為了對資源通路實施同步,Wi n d o w s提供了許多基本要素來幫助進行操作,如事件、信标、互斥對象和關鍵代碼段等。這些基本要素的使用都非常友善。為了使操作變得更加友善,唯一的方法是讓系統能夠自動保護共享資源。不幸的是,在Wi n d o w s提供一種讓人滿意的保護方法之前,我們已經有了一種這樣的方法。
在如何對線程的建立和撤消進行管理的問題上,人人都有自己的好主意。近年來,我自己建立了若幹不同的線程池實作代碼,每個實作代碼都進行了很好的調整,以便适應特定環境的需要。M i c r o s o f t公司的Windows 2000提供了一些新的線程池函數,使得線程的建立、撤消和基本管理變得更加容易。這個新的通用線程池并不完全适合每一種環境,但是它常常可以适合你的需要,并且能夠節省大量的程式開發時間。
新的線程池函數使你能夠執行下列操作:
• 異步調用函數。
• 按照規定的時間間隔調用函數。
• 當單個核心對象變為已通知狀态時調用函數。
• 當異步I / O請求完成時調用函數。
為了完成這些操作,線程池由4個獨立的部分組成。表11 - 1顯示了這些元件并描述了控制其行為特性的規則。
表11-1 線程池的元件及其行為特性
元件
定時器
等待
I / O
非I / O
線程的初始數值
總是1
當建立一個線程時
當調用第一個線程池函數時
每63個注冊對象有一個線程
系統使用試探法,但是這裡有一些因素會影響線程的建立:•自從添加線程後已經過去一定的時間(以秒計算)•使用WT_EXECUTELONGFUNCTION 标志•已經排隊的工作項目的數量超過了某個門檻值
當線程被撤消時
當程序終止運作時
當已經注冊的等待對象數量是0時
當線程沒有未處理的I / O請求并且已經空閑了一個門檻值周期(約1 m s )時
當線程空閑了一個門檻值周期( 約1 m s )時
線程如何等待
待命狀态
Wa i t F o r M u l t i p l e O bj e c t s
G e t Q u e u e d - C o m p l et i o n - S t a t u s
是什麼喚醒了線程
等待定時器通知排隊的使用者A P C
核心對象變為已通知狀态
排隊的使用者A P C和已完成的I / O請求
展示已完成的狀态和I / O請示(完成端口最多允許數量為2 *的C P U線程同時運作的數量)
當程序初始化時,它并不産生與這些元件相關聯的任何開銷。但是,一旦新線程池函數之一被調用時,就為程序建立某些元件,并且其中有些元件将被保留,直到程序終止運作為止。如你所見,使用線程池所産生的開銷并不小。相當多的線程和内部資料結構變成了你的程序的一個組成部分。是以必須認真考慮線程池能夠為你做什麼和不能做什麼,不要盲目地使用這些函數。
好了,上述說明已經足夠了。下面讓我們來看一看這些函數能夠做些什麼。
<a></a>
11.1 方案1:異步調用函數
假設有一個伺服器程序,該程序有一個主線程,正在等待客戶機的請求。當主線程收到該請求時,它就産生一個專門的線程,以便處理該請求。這使得應用程式的主線程循環運作,并等待另一個客戶機的請求。這個方案是客戶機/伺服器應用程式的典型實作方法。雖然它的實作方法非常明确,但是也可以使用新線程池函數來實作它。
當伺服器程序的主線程收到客戶機的請求時,它可以調用下面這個函數:
該函數将一個“工作項目”排隊放入線程池中的一個線程中并且立即傳回。所謂工作項目是指一個(用p f n C a l l b a c k參數辨別的)函數,它被調用并傳遞單個參數p v C o n t e x t。最後,線程池中的某個線程将處理該工作項目,導緻函數被調用。所編的回調函數必須采用下面的原型:
盡管必須使這個函數的原型傳回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提供了一個等待定時器核心對象,是以可以友善地獲得基于時間的通知。許多程式員為應用程式執行的每個基于時間的操作任務建立了一個等待定時器對象,但是這是不必要的,會浪費系統資源。相反,可以建立一個等待定時器,将它設定為下一個預定運作的時間,然後為下一個時間重置定時器,如此類推。然而,要編寫這樣的代碼非常困難,不過可以讓新線程池函數對此進行管理。
若要排程在某個時間運作的工作項目,首先要調用下面的函數,建立一個定時器隊列:
定時器隊列對一組定時器進行組織安排。例如,有一個可執行檔案控制着若幹個服務程式。每個服務程式需要觸發定時器,以幫助保持它的狀态,比如客戶機何時不再作出響應,何時收集和更新某些統計資訊等。讓每個服務程式占用一個等待定時器和專用線程,這是不經濟的。相反,每個服務程式可以擁有它自己的定時器隊列(這是個輕便的資源),并且共享定時器元件的線程和等待定時器對象。當一個服務程式終止運作時,它隻需要删除它的定時器隊列即可,因為這會删除該隊列建立的所有定時器。
一旦擁有一個定時器隊列,就可以在該隊列中建立下面的定時器:
對于第二個參數,可以傳遞想要在其中建立定時器的定時器隊列的句柄。如果隻是建立少數幾個定時器,隻需要為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參數傳回。
工作回調函數必須采用下面的原型:
當該函數被調用時,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将被忽略。
當不再想要觸發定時器時,必須通過調用下面的函數将它删除:
即使對于已經觸發的單步定時器,也必須調用該函數。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來傳回。
最後一個參數h C o m p l e t i o n E v e n t告訴你,由于該定時器,什麼時候将不再存在沒有處理的已排隊的工作項目。如果為該參數傳遞I N VA L I D _ H A N D L E _ VA L U E,那麼在該定時器的所有已排隊工作項目完成運作之前, D e l e t e Ti m e r Q u e u e Ti m e r函數不會傳回。請想一想這将意味着什麼。如果在定時器處理自己的工作項目期間對定時器進行一次中斷删除,就會造成一個死鎖條件。雖然你正在等待工作項目完成處理操作,但是你在等待它完成操作時卻中斷了它的處理。隻有當線程不是處理定時器的工作項目的線程時,該線程才能進行對定時器的中斷删除。
另外,如果你正在使用定時器元件的線程,不應該試圖對任何定時器進行中斷删除,否則就會産生死鎖。如果試圖删除一個定時器,就會将一個A P C通知放入該定時器元件的線程隊列中。如果該線程正在等待一個定時器被删除,而它不能删除該定時器,那麼就會發生死鎖。
如果不為h C o m p l e t i o n E v e n t參數傳遞I N VA L I D _ H A N D L E _ VA L U E,可以傳遞N U L L。這将告訴該函數,你想盡快删除定時器。在這種情況下, 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将立即傳回,同時,當定時器的所有已經排隊的工作項目完成運作之後,定時器元件的線程将設定該事件。在調用D e l e t e Ti m e r Q u e u e Ti m e r之前,千萬不要給該事件發送通知,否則你的代碼将認為排隊的工作項目已經完成運作,但是實際上它們并沒有完成。
一旦建立了一個定時器,可以調用下面這個函數來改變它的到期時間和到期周期:
這裡傳遞了定時器隊列的句柄和想要修改的現有定時器的句柄。可以修改定時器的d w D u e Ti m e和d w P e r i o d。注意,試圖修改已經觸發的單步定時器是不起作用的。另外,你可以随意調用該函數,而不必擔心死鎖。
當不再需要一組定時器時,可以調用下面這個函數,删除定時器隊列:
該函數取出一個現有的定時器隊列的句柄,并删除它裡面的所有定時器,這樣就不必為删除每個定時器而顯式調用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函數中的語義是相同的。這意味着它存在同樣的死鎖可能性,是以必須小心。
在開始介紹另一個方案之前,讓我們說明兩個其他的項目。首先,線程池的定時器元件建立等待定時器,這樣,它就可以給A P C項目排隊,而不是給對象發送通知。這意味着作業系統能夠連續給A P C項目排隊,并且定時器事件從來不會丢失。是以,設定一個定期定時器能夠保證每個間隔時間都能為你的工作項目排隊。如果建立一個定期定時器,每隔1 0 s觸發一次,那麼每隔1 0 s就調用你的回調函數。必須注意這在使用多線程時也會發生必須對工作項目函數的各個部分實施同步。
如果不喜歡這種行為特性,而希望你的工作項目在每個項目執行之後的1 0 s進行排隊,那麼應該在工作項目函數的結尾處建立單步定時器。或者可以建立一個帶有高逾時值的單個定時器,并在工作項目函數的結尾處調用C h a n g e Ti m e r Q u e u e Ti m e r.
Ti m e d M s g B o x示例應用程式
清單11 - 1列出的Ti m e d M s g B o x應用程式(“11 Ti m e d M s g B o x . e x e”)顯示了如何使用線程池的定時器函數來實作一個使用者在規定時間内不作出響應時能自動關閉的消息框。該應用程式的源代碼和資源檔案位于本書所附CD光牒上的11 - Ti m e d M s g B o x目錄下。
當啟動該程式時,它将全局變量g _ n S e c L e f t設定為1 0。這表示使用者必須在規定時間内對消息框作出響應的秒數。然後調用C r e a t e Ti m e r Q u e u e Ti m e r函數,指令線程池每秒鐘調用一次M s g B o x Ti m e o u t函數。一旦一切都已初始化,便調用M e s s a g e B o x,并向使用者顯示圖11 - 1所示的消息框。
在等待使用者作出響應的時候,線程池中的一個線程便調用M s g B o x Ti m e o u t函數。該函數尋找消息框的視窗句柄,對全局變量g _ n S e c L e f t進行遞減,并更新消息框中的字元串。當M s g B o x Ti m e o u t第一次被調用後,消息框就類似下面的樣子(見圖11 - 2 )。
當M s g B o x Ti m e o u t第1 0次被調用時, g _ n S e c L e f t變量變為0,同時M s g B o x Ti m e o u t調用E n d D i a l o g函數來撤消該消息框。主線程調用的M e s s a g e B o x傳回,D e l e t e Ti m e r Q u e u e Ti m e r被調用,以告訴線程池停止調用M s g B o x Ti m e o u t函數,這時出現圖11 - 3所示的消息框,告訴使用者他沒有在配置設定給他的時間内對圖11 - 1所示的消息框作出響應。
如果使用者沒有在時間到期之前作出響應,便出現圖11 - 4所示的消息框。
清單11-1 Ti m e d M s g B o x示例應用程式
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元件的線程中。這些線程之一最終将會醒來,并且調用你的函數,該函數的原型必須是下面的形式:
如果等待逾時了,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标志注冊的并且已經擁有隊列的工作項目的等待元件,情況也是如此。調用下面這個函數,可以取消等待元件的注冊狀态:
第一個參數指明一個注冊的等待(由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請求。該完成例程必須采用下面的原型:
你将會注意到沒有将一個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标志和想要的任何資料(至少是重疊結構)。這就是線程池函數能夠為你執行的全部功能。