Windows核心编程 第九章 线程与内核对象的同步(上)
第9章?線程與內核對象的同步
? ? 上一章介紹了如何使用允許線程保留在用戶方式中的機制來實現線程同步的方法。用戶方式同步的優點是它的同步速度非常快。如果強調線程的運行速度,那么首先應該確定用戶方式的線程同步機制是否適合需要。
? ? 雖然用戶方式的線程同步機制具有速度快的優點,但是它也有其局限性。對于許多應用程序來說,這種機制是不適用的。例如,互鎖函數家族只能在單值上運行,根本無法使線程進入等待狀態。可以使用關鍵代碼段使線程進入等待狀態,但是只能用這些代碼段對單個進程中的線程實施同步。還有,使用關鍵代碼段時,很容易陷入死鎖狀態,因為在等待進入關鍵代碼段時無法設定超時值。
? ? 本章將要介紹如何使用內核對象來實現線程的同步。你將會看到,內核對象機制的適應性遠遠優于用戶方式機制。實際上,內核對象機制的唯一不足之處是它的速度比較慢。當調用本章中提到的任何新函數時,調用線程必須從用戶方式轉為內核方式。這個轉換需要很大的代價:往返一次需要占用 x 8 6平臺上的大約1 0 0 0個C P U周期,當然,這還不包括執行內核方式代碼,即實現線程調用的函數的代碼所需的時間。
? ? 本書介紹了若干種內核對象,包括進程,線程和作業。可以將所有這些內核對象用于同步目的。對于線程同步來說,這些內核對象中的每種對象都可以說是處于已通知或未通知的狀態之中。這種狀態的切換是由M i c r o s o f t為每個對象建立的一套規則來決定的。例如,進程內核對象總是在未通知狀態中創建的。當進程終止運行時,操作系統自動使該進程的內核對象處于已通知狀態。一旦進程內核對象得到通知,它將永遠保持這種狀態,它的狀態永遠不會改為未通知狀態。
? ? 當進程正在運行的時候,進程內核對象處于未通知狀態,當進程終止運行的時候,它就變為已通知狀態。進程內核對象中是個布爾值,當對象創建時,該值被初始化為 FA L S E(未通知狀態) 。當進程終止運行時,操作系統自動將對應的對象布爾值改為 T R U E,表示該對象已經得到通知。
? ? 如果編寫的代碼是用于檢查進程是否仍在運行,那么只需要調用一個函數,讓操作系統去
? ? 檢查進程對象的布爾值,這非常簡單。你也可能想要告訴系統使線程進入等待狀態,然后當布爾值從FA L S E改為T R U E時自動喚醒該線程。這樣,你可以編寫一個代碼,在這個代碼中,需要等待子進程終止運行的父進程中的線程只需要使自己進入睡眠狀態,直到標識子進程的內核對象變為已通知狀態即可。你將會看到, M i c r o s o f t的Wi n d o w s提供了一些能夠非常容易地完成這些操作的函數。
? ? 剛才講了M i c r o s o f t為進程內核對象定義了一些規則。實際上,線程內核對象也遵循同樣的規則。即線程內核對象總是在未通知狀態中創建。當線程終止運行時,操作系統會自動將線程對象的狀態改為已通知狀態。因此,可以將相同的方法用于應用程序,以確定線程是否不再運行。與進程內核對象一樣,線程內核對象也可以處于已通知狀態或未通知狀態。
????下面的內核對象可以處于已通知狀態或未通知狀態:
????■ 進程 ■ 文件修改通知
????■ 線程 ■ 事件
????■ 作業 ■ 可等待定時器
????■ 文件 ■ 信標
????■ 控制臺輸入 ■ 互斥對象
? ? 線程可以使自己進入等待狀態,直到一個對象變為已通知狀態。注意,用于控制每個對象的已通知/未通知狀態的規則要根據對象的類型而定。前面已經提到進程和線程對象的規則及作業的規則。
9.1 等待函數
? ? 等待函數可使線程自愿進入等待狀態,直到一個特定的內核對象變為已通知狀態為止。這些等待函數中最常用的是Wa i t F o r S i n g l e O b j e c t :
DWORD WaitForSingObject(
HANDLE hObject,
DWORD dwMilliseconds);
? ? 當線程調用該函數時,第一個參數 h O b j e c t標識一個能夠支持被通知 /未通知的內核對象(前面列出的任何一種對象都適用) 。第二個參數d w M i l l i s e c o n d s允許該線程指明,為了等待該對象變為已通知狀態,它將等待多長時間。
? ? 調用下面這個函數將告訴系統,調用函數準備等待到h P r o c e s s句柄標識的進程終止運行為止:
? ? WaitForSingleObject(hProcess ,INFINITE);
? ? 第二個參數告訴系統,調用線程愿意永遠等待下去(無限時間量) ,直到該進程終止運行。
? ? 通常情況下,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的,不過也可以傳遞任何一個值(以毫秒計算) 。順便說一下,I N F I N I T E已經定義為0 x F F F F F F F F(或-1) 。當然,傳遞I N F I N I T E有些危險。如果對象永遠不變為已通知狀態,那么調用線程永遠不會被喚醒,它將永遠處于死鎖狀態,不過,它不會浪費寶貴的C P U時間。
? ? 下面是如何用一個超時值而不是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的例子:
?
? ? 上面這個代碼告訴系統,在特定的進程終止運行之前,或者在 5 0 0 0 m s時間結束之前,調用線程不應該變為可調度狀態。因此,如果進程終止運行,那么這個函數調用將在不到5 0 0 0 m s的時間內返回,如果進程尚未終止運行,那么它在大約 5 0 0 0 m s時間內返回。注意,不能為d w M i l l i s e c o n d傳遞0。如果傳遞了0,Wa i t F o r S i n g l e O b j e c t函數將總是立即返回。
? ? Wa i t F o r S i n g l e O b j e c t的返回值能夠指明調用線程為什么再次變為可調度狀態。如果線程等待的對象變為已通知狀態,那么返回值是 WA I T _ O B J E C T _ 0。如果設置的超時已經到期,則返回值是WA I T _ T I M E O U T。如果將一個錯誤的值(如一個無效句柄)傳遞給 Wa i t F o r S i n g l eO b j e c t,那么返回值將是WA I T _ FA I L E D(若要了解詳細信息,可調用G e t L a s t E r r o r) 。
? ? 下面這個函數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 S i n g l e O b j e c t函數很相似,區別在于它允許調用線程同時查看若干個內核對象的已通知狀態:
DWORD WaitForMultipleObjects(
????DWORD dwCount,
????CONST HANDLE*phObjects,
????BOOL fWaitAll,
????DWORD dwMilliseconds
);
? ? ?d w C o u n t參數用于指明想要讓函數查看的內核對象的數量。這個值必須在 1與M A X I M U M _WA I T _ O B J E C T S(在Wi n d o w s頭文件中定義為6 4)之間。p h 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函數。一種方式是讓線程進入等待狀態,直到指定內核對象中的任何一個變為已通知狀態。另一種方式是讓線程進入等待狀態,直到所有指定的內核對象都變為已通知狀態。 f Wa i tAl l參數告訴該函數,你想要讓它使用何種方式。如果為該參數傳遞T R U E,那么在所有對象變為已通知狀態之前,該函數將不允許調用線程運行。
? ? d w M i l l i s e c o n d s參數的作用與它在Wa i t F o r S i n g l e O b j e c t中的作用完全相同。如果在等待的時候規定的時間到了,那么該函數無論如何都會返回。同樣,通常為該參數傳遞 I N F I N I T 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 _ FA I L E D和WA I T _ T I M E O U T,這兩個值的作用是很清楚的。如果為 f Wa i tAl l參數傳遞T R U E,同時所有對象均變為已通知狀態,那么返回值是WA I T _ O B J E C T _ 0。如果為f Wa i t A l l傳遞FA L S E,那么一旦任何一個對象變為已通知狀態,該函數便返回。在這種情況下,你可能想要知道哪個對象變為已通知狀態。返回值是WA I T _ O B J E C T _ 0與(WA I T _ O B J E C T _ 0 + d w C o u n t - 1)之間的一個值。換句話說,如果返回值不是WA I T _ T I M E O U T,也不是WA I T _ FA I L E D,那么應該從返回值中減去WA I T _ O B J E C T _ 0。產生的數字是作為第二個參數傳遞給Wa i t F o r M u l t i p l e O b j e c t s的句柄數組中的索引。該索引說明哪個對象變為已通知狀態。下面是說明這一情況的一些示例代碼:
?
? ? 如果為f Wa i t A l l參數傳遞FA L S E,Wa i t F o r M u l t i p l e O b j e c t s就從索引0開始向上對句柄數組進行掃描,同時已通知的第一個對象終止等待狀態。這可能產生一些你不希望有的結果。例如,通過將3個進程句柄傳遞給該函數,你的線程就會等待 3個子進程終止運行。如果數組中索引為0的進程終止運行,Wa i t F o r M u l t i p l e O b j e c t s就會返回。這時該線程就可以做它需要的任事情,然后循環反復,等待另一個進程終止運行。如果該線程傳遞相同的 3個句柄,該函數立即再次返回WA I T _ O B J E C T _ 0。除非刪除已經收到通知的句柄,否則代碼就無法正確地運行。
9.2 成功等待的副作用
? ? 對于有些內核對象來說,成功地調用 Wa i t F o r S i n g l e 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,實際上會改變對象的狀態。成功地調用是指函數發現對象已經得到通知并且返回一個相對于WA I T _ O B J E C T _ 0的值。如果函數返回WA I T _ T I M E O U T或WA I T _ FA I L E D,那么調用就沒有成功。如果函數調用沒有成功,對象的狀態就不可能改變。
? ? 當一個對象的狀態改變時,我稱之為成功等待的副作用。例如,有一個線程正在等待自動清除事件對象(本章后面將要介紹) 。當事件對象變為已通知狀態時,函數就會發現這個情況,并將WA I T _ O B J E C T _ 0返回給調用線程。但是就在函數返回之前,該事件將被置為未通知狀態,這就是成功等待的副作用。
這個副作用將用于自動清除內核對象,因為它是 M i c r o s o f t為這種類型的對象定義的規則之一。其他對象擁有不同的副作用,而有些對象則根本沒有任何副作用。進程和線程內核對象就根本沒有任何副作用,也就是說,在這些對象之一上進行等待決不會改變對象的狀態。由于本章要介紹各種不同的內核對象,因此我們將要詳細說明它們的成功等待的副作用。
? ? 究竟是什么原因使得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函數時,該函數能夠測試所有對象的通知狀態,并且能夠將所有必要的副作用作為一項操作來執行。
讓我們觀察一個例子。兩個線程以完全相同的方式來調用 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函數被調用時,兩個事件都處于未通知狀態,這就迫使兩個線程都進入等待狀態。然后h A u t o R e s e t E v e n t 1對象變為已通知狀態。兩個線程都發現,該事件已經變為已通知狀態,但是它們都無法被喚醒,因為 h A u t o R e s e t E v e n t 2仍然處于未通知狀態。由于兩個線程都沒有等待成功,因此沒有對h A u t o R e s e t E v e n t 1對象產生任何副作用。
? ? 接著,h A u t o R e s e t E v e n t 2變為已通知狀態。這時,兩個線程中的一個發現,兩個對象都變為已通知狀態。等待取得了成功,兩個事件對象均被置為未通知狀態,該線程變為可調度的線程。但是另一個線程的情況如何呢?它將繼續等待,直到它發現兩個事件對象都處于已通知狀態。盡管它原先發現 h A u t o R e s e t E v e n t 1處于已通知狀態,但是現在它將該對象視為未通知狀態。
? ? 前面講過,有一個重要問題必須注意,即Wa i t F o r M u l t i p l e O b j e c t s是以原子操作方式運行的。當它檢查內核對象的狀態時,其他任何線程都無法背著對象改變它的狀態。這可以防止出現死鎖情況。試想,如果一個線程看到h A u t o R e s e t E v e n t 1已經得到通知并將事件重置為未通知狀態,然后,另一個線程發現h A u t o R e s e t E v e n t 2已經得到通知并將該事件重置為未通知狀態,那么這兩個線程均將被凍結:一個線程將等待另一個線程已經得到的對象,另一個線程將等待該線程已經得到的對象。Wa i t F o r M u l t i p l e O b j e c t s能夠確保這種情況永遠不會發生。
? ? 這會產生一個非常有趣的問題,即如果多個線程等待單個內核對象,那么當該對象變成已通知狀態時,系統究竟決定喚醒哪個線程呢? M i c r o s o f t對這個問題的正式回答是: “算法是公平的。 ”M i c r o s o f t不想使用系統使用的內部算法。它只是說該算法是公平的,這意味著如果多個線程正在等待,那么每當對象變為已通知狀態時,每個線程都應該得到它自己的被喚醒的機會。
? ? 這意味著線程的優先級不起任何作用,即高優先級線程不一定得到該對象。這還意味著等待時間最長的線程不一定得到該對象。同時得到對象的線程有可能反復循環,并且再次得到該對象。但是,這對于其他線程來說是不公平的,因此該算法將設法防止這種情況的出現。但是這不一定做得到。
? ? 在實際操作中,M i c r o s o f t使用的算法是常用的“先進先出”的方案。等待了最長時間的線程將得到該對象。但是系統中將會執行一些操作,以便改變這個行為特性,使它不太容易預測。這就是為什么M i c r o s o f t沒有明確說明該算法如何起作用的原因。操作之一是讓線程暫停運行。如果一個線程等待一個對象,然后該線程暫停運行,那么系統就會忘記該線程正在等待該對象。這是一個特性,因為沒有理由為一個暫停運行的線程進行調度。當后來該線程恢復運行時,系統將認為該線程剛剛開始等待該對象。
? ? 當調試一個進程時,只要到達一個斷點,該進程中的所有線程均暫停運行。因此,調試一個進程會使“先進先出”的算法很難預測其結果,因為線程常常暫停運行,然后再恢復運行。
9.3 事件內核對象
? ??在所有的內核對象中,事件內核對象是個最基本的對象。它們包含一個使用計數(與所有內核對象一樣) ,一個用于指明該事件是個自動重置的事件還是一個人工重置的事件的布爾值,另一個用于指明該事件處于已通知狀態還是未通知狀態的布爾值。
? ? 事件能夠通知一個操作已經完成。有兩種不同類型的事件對象。一種是人工重置的事件,另一種是自動重置的事件。當人工重置的事件得到通知時,等待該事件的所有線程均變為可調度線程。當一個自動重置的事件得到通知時,等待該事件的線程中只有一個線程變為可調度線程。
? ? 當一個線程執行初始化操作,然后通知另一個線程執行剩余的操作時,事件使用得最多。事件初始化為未通知狀態,然后,當該線程完成它的初始化操作后,它就將事件設置為已通知狀態。這時,一直在等待該事件的另一個線程發現該事件已經得到通知,因此它就變成可調度線程。這第二個線程知道第一個線程已經完成了它的操作。
? ? 下面是C r e a t e E v e n t函數,用于創建事件內核對象:
HANDLE CrreateEvent(
??PSECURITY_ATTRIBUTES psa,
??BOOL fManualReset,
??BOOL fInitialState,
??PCTSTR pszName
);
? ? 第3章已經介紹了內核對象的操作技巧,比如,如何設置它們的安全性,如何進行使用計數,如何繼承它們的句柄,如何按名字共享對象等。由于現在你對所有這些對象都已經熟悉了,所以不再介紹該函數的第一個和最后一個參數。
? ? F M a n n u a l R e s e t參數是個布爾值,它能夠告訴系統是創建一個人工重置的事件( T R U E)還是創建一個自動重置的事件( FA L S E) 。f I n i t i a l S t a t e參數用于指明該事件是要初始化為已通知狀態(T R U E)還是未通知狀態(FA L S E) 。當系統創建事件對象后,c r e a t e E v e n t就將與進程相關的句柄返回給事件對象。其他進程中的線程可以獲得對該對象的訪問權,方法是使用在p s z N a m e參數中傳遞的相同值,使用繼承性,使用 D u p l i c a t e H a n d l e函數等來調用C r e a t e E v e n t,或者調用O p e n E v e n t ,在p s z N a m e參數中設定一個與調用C r e a t e E v e n t時設定的名字相匹配的名字:
HANDLE OpenEvent(
????DWORD fdwAccess,
????BOOL fInherit,
????PCTSTR pszName
);
? ? 與所有情況中一樣,當不再需要事件內核對象時,應該調用C l o s e H a n d l e函數。
? ? 一旦事件已經創建,就可以直接控制它的狀態。當調用 S e t E v e n t時,可以將事件改為已通知狀態:
? ? BOOL SetEvent(HANDLE hEvent);
? ? 當調用R e s e t E v e n t函數時,可以將該事件改為未通知狀態:
? ? BOOL ResetEvent(HANDLE hEvent);
? ? M i c r o s o f t為自動重置的事件定義了應該成功等待的副作用規則,即當線程成功地等待到該對象時,自動重置的事件就會自動重置到未通知狀態。這就是自動重置的事件如何獲得它們的名字的方法。通常沒有必要為自動重置的事件調用 R e s e t E v e n t函數,因為系統會自動對事件進行重置。但是,M i c r o s o f t沒有為人工重置的事件定義成功等待的副作用。
? ? 讓我們觀察一個簡單的例子,以便說明如何使用事件內核對象對線程進行同步。下面就是這個代碼:
?
????當這個進程啟動時,它創建一個人工重置的未通知狀態的事件,并且將句柄保存在一個全局變量中。這使得該進程中的其他線程能夠非常容易地訪問同一個事件對象。現在 3個線程已經產生。這些線程要等待文件的內容讀入內存,然后每個線程都要訪問它的數據。一個線程進行單詞計數,另一個線程運行拼寫檢查器,第三個線程運行語法檢查器。這 3個線程函數的代碼的開始部分都相同,每個函數都調用 Wa i t F o r S i n g l e O b j e c t,這將使線程暫停運行,直到文件的內容由主線程讀入內存為止。
? ? 一旦主線程將數據準備好,它就調用 S e t E v e n t,給事件發出通知信號。這時,系統就使所有這3個輔助線程進入可調度狀態,它們都獲得了C P U時間,并且可以訪問內存塊。注意,這3個線程都以只讀方式訪問內存。這就是所有 3個線程能夠同時運行的唯一原因。還要注意,如何計算機上配有多個C P U,那么所有3個線程都能夠真正地同時運行,從而可以在很短的時間內完成大量的操作。
? ? 如果你使用自動重置的事件而不是人工重置的事件,那么應用程序的行為特性就有很大的差別。當主線程調用S e t E v e n t之后,系統只允許一個輔助線程變成可調度狀態。同樣,也無法保證系統將使哪個線程變為可調度狀態。其余兩個輔助線程將繼續等待。
? ? 已經變為可調度狀態的線程擁有對內存塊的獨占訪問權。讓我們重新編寫線程的函數,使得每個函數在返回前調用S e t E v e n t函數(就像Wi n M a i n函數所做的那樣) 。這些線程函數現在變成下面的形式:
?
? ? 當線程完成它對數據的專門傳遞時,它就調用 S e t E v e n t函數,該函數允許系統使得兩個正在等待的線程中的一個成為可調度線程。同樣,我們不知道系統將選擇哪個線程作為可調度線程,但是該線程將進行它自己的對內存塊的專門傳遞。當該線程完成操作時,它也將調用S e t E v e n t函數,使第三個即最后一個線程進行它自己的對內存塊的傳遞。注意,當使用自動重置事件時,如果每個輔助線程均以讀 /寫方式訪問內存塊,那么就不會產生任何問題,這些線程將不再被要求將數據視為只讀數據。這個例子清楚地展示出使用人工重置事件與自動重置事之間的差別。
? ? 為了完整起見,下面再介紹一個可以用于事件的函數:
? ? BOOL PulseEvent(HANDLE hEvent);
? ? P u l s e E v e n t函數使得事件變為已通知狀態,然后立即又變為未通知狀態,這就像在調用S e t E v e n t后又立即調用R e s e t E v e n t函數一樣。如果在人工重置的事件上調用P u l s e E v e n t函數,那么在發出該事件時,等待該事件的任何一個線程或所有線程將變為可調度線程。如果在自動重置事件上調用P u l s e E v e n t函數,那么只有一個等待該事件的線程變為可調度線程。如果在發出事件時沒有任何線程在等待該事件,那么將不起任何作用。
? ? P u l s e E v e n t函數并不非常有用。實際上我在自己的應用程序中從未使用它,因為根本不知道什么線程將會看到事件的發出并變成可調度線程。由于在調用 P u l s e E v e n t時無法知道任何線程的狀態,因此該函數并不那么有用。我相信在有些情況下,雖然 P u l s e E v e n t函數可以方便地供你使用,但是你根本想不起要去使用它。關于 P u l s e E v e n t函數的比較詳細的說明,請參見本章后面對S i n g l e O b j e c t A n d Wa i t函數的介紹。
總結
以上是生活随笔為你收集整理的Windows核心编程 第九章 线程与内核对象的同步(上)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows核心编程 第八章 用户方式
- 下一篇: Windows核心编程 第九章 线程与内