Windows核心编程 第九章 线程与内核对象的同步(下)
9.4 等待定時器內核對象
? ? 等待定時器是在某個時間或按規定的間隔時間發出自己的信號通知的內核對象。它們通常用來在某個時間執行某個操作。
? ? 若要創建等待定時器,只需要調用C r e a t e Wa i t a b l e Ti m e r函數:
HANDLE CreateWaitableTimer(
??PSECURITY_ATTRIBUTES psa,
??BOOL fManualReset,
??PCTSTR pszName
);
p s a和p s z N a m e這兩個參數在第3章中做過介紹。當然,進程可以獲得它自己的與進程相關的現有等待定時器的句柄,方法是調用O p e n Wa i t a b l e Ti m e r函數:
HANDLE OpenWaitableTimer(
????DWORD dwDesiredAccess,
????BOOL bInheritHandle,
????PCTSTR pszName
);
? ? 與事件的情況一樣,f M a n u a l R e s e t參數用于指明人工重置的定時器或自動重置的定時器。當發出人工重置的定時器信號通知時,等待該定時器的所有線程均變為可調度線程。當發出自動重置的定時器信號通知時,只有一個等待的線程變為可調度線程。
? ? 等待定時器對象總是在未通知狀態中創建。必須調用 S e t Wa i t a b l e Ti m e r函數來告訴定時器你想在何時讓它成為已通知狀態:
?
? ? 這個函數帶有若干個參數,使用時很容易搞混。顯然, h Ti m e r參數用于指明你要設置的定時器。p D u e Ti m e和l P e r i o d兩個參數是一道使用的。P D u e Ti m e r參數用于指明定時器何時應該第一次報時,而l P e r i o d參數則用于指明此后定時器應該間隔多長時間報時一次。下面的代碼用于將定時器的第一次報時的時間設置在2 0 0 2年1月1日的下午1點鐘,然后每隔6小時報時一次:
?
? ? 代碼首先對S Y S T E M T I M E結構進行初始化,該結構用于指明定時器何時第一次報時(發出信號通知) 。我將該時間設置為本地時間,即計算機所在時區的正確時間。 S e t Wa i t a b l e Ti m e r的第二個參數的原型是個常量LARGE_INTEGER *,因此它不能直接接受S Y S T E M T I M E結構。但是,F I L E T I M E結構和L A R G E _ I N T E G E R結構擁有相同的二進制格式,都包含兩個 3 2位的值。因此,我們可以將S Y S T E M T I M E結構轉換成F I L E T I M E結構。再一個問題是,S e t Wa i t a b l e Ti m e r希望傳遞給它的時間始終都采用世界協調時( U T C)的時間。調用L o c a l F i l e Ti m e To F i l e Ti m e函數,就可以很容易地進行時間的轉換。
? ? 由于F I L E T I M E和L A R G E _ I N T E G E R結構具有相同的二進制格式,因此可以像下面這樣將F I L E T I M E結構的地址直接傳遞給S e t Wa i t a b l e Ti m e r:
?
? ? 實際上,這是我最初的做法。但是這是個大錯誤。雖然 F I L E T I M E和L A R G E _ I N T E G E R結構采用相同的二進制格式,但是這兩個結構的調整要求不同。所有 F I L E T I M E結構的地址必須從一個3 2位的邊界開始,而所有 L A R G E _ I N T E G E R結構的地址則必須從 6 4位的邊界開始。調用S e t Wa i t a b l e Ti m e r函數和給它傳遞一個 F I L E T I M E結構時是否能夠正確地運行,取決于F I L E T I M E結構是否恰好位于6 4位的邊界上。但是,編譯器能夠確保L A R G E _ I N T E G E R結構總是從6 4位的邊界開始,因此要進行的正確操作(也就是所有時間都能保證起作用的操作)是將F I L E T I M E的成員拷貝到L A R G E _ I N T E G E R的成員中,然后將L A R G E _ I N T E G E R的地址傳遞給S e t Wa i t a b l e Ti m e r。
? ? 注意 x 8 6處理器能夠悄悄地處理未對齊的數據引用。因此當應用程序在 x86 CPU上運行時,將F I L E T I M E的地址傳遞給S e t Wa i t a b l r Ti m e r總是可行的。但是,其他處理器,如A l p h a處理器,則無法像x 8 6處理器那樣悄悄地處理未對齊的數據引用。實際上,大多數其他處理器都會產生一個 E X C E P T I O N _ D ATAT Y P E _ M I S A L I G N M E N T異常,它會導致進程終止運行。當你將 x 8 6計算機上運行的代碼移植到其他處理器時,產生問題的最大原因是出現了對齊錯誤。如果現在注意對齊方面的問題,就能夠在以后省去幾個月的代碼移植工作。關于對齊問題的詳細說明,參見第 1 3章。
? ? 現在,若要使定時器在2 0 0 2年1月1日下午1點之后每隔6 h進行一次報時,我們應該將注意力轉向l P e r i o d參數。該參數用于指明定時器在初次報時后每隔多長時間(以毫秒為單位)進行一次報時。如果是每隔6 h進行一次報時,那么我傳遞 21 600 000(6 h×每小時6 0 m i n×每分鐘6 0 s×每秒1 0 0 0 m s) 。另外,如果給它傳遞了以前的一個絕對時間,比如1 9 7 5年1月1日下午1點,那么S e t Wa i t a b l e Ti m e r的運行就會失敗。
? ? 如果不設置定時器應該第一次報時的絕對時間,也可以讓定時器在一個相對于調用S e t Wa i t a b l e Ti m e r的時間進行報時。只需要在p D u e Ti m e參數中傳遞一個負值。傳遞的值必須是以1 0 0 n s為間隔。由于我們通常并不以 1 0 0 n s的間隔來思考問題,因此我們要說明一下 1 0 0 n s的具體概念:1 s = 1 0 0 0 m s = 1 0 0 0 0 0 0 μ s = 1 0 0 0 0 0 0 0 0 0 n s 。
下面的代碼用于將定時器設置為在調用S e t Wa i t a b l e Ti m e r函數后5 s第一次報時:
?
? ? 通常情況下,你可能想要一個一次報時的定時器,它只是發出一次報時信號,此后再也不發出報時信號。若要做到這一點,只需要為 l P e r i o d參數傳遞0即可。然后可以調用C l o s e H a n d l e函數,關閉定時器,或者再次調用 S e t Wa i t a b l e Ti m e r函數,重新設置時間,為它規定一個需要遵循的新條件。
? ? S e t Wa i t a b l e Ti m e r的最后一個參數是f R e s u m e,它可以用于支持暫停和恢復的計算機。通常可以為該參數傳遞FA L S E,就像我在上面這個代碼段中設置的那樣。但是,如果你編寫了一個會議安排類型的應用程序,在這個應用程序在中,你想設置一個為用戶提醒會議時間安排的定時器,那么應該傳遞T R U E。當定時器報時的時候,它將使計算機擺脫暫停方式(如果它處于暫停狀態的話) ,并喚醒等待定時器報時的線程。然后該應用程序運行一個波形文件,并顯示一個消息框,告訴用戶即將舉行的會議。如果為 f R e s u m e參數傳遞FA L S E,定時器對象就變為已通知狀態,但是它喚醒的線程必須等到計算機恢復運行(通常由用戶將它喚醒)之后才能得到C P U時間。
除了上面介紹的定時器函數外,最后還有一個C a n c e l Wa i t a b l e Ti m e r函數:
? ? BOOL CancelWaitableTimer(HANDLE hTimer);
這個簡單的函數用于取出定時器的句柄并將它撤消,這樣,除非接著調用 S e t Wa i t a b l e Ti m e r函數以便重新設置定時器,否則定時器決不會進行報時。如果想要改變定時器的報時條件,不必在調用S e t Wa i t a b l e Ti m e r函數之前調用C a n c e l Wa i t a b l e Ti m e r函數。每次調用S e t Wa i t a b l e Ti m e r函數,都會在設置新的報時條件之前撤消定時器原來的報時條件。
9.4.1 讓等待定時器給A P C項排隊
? ??到現在為止,你已經學會了如何創建定時器和如何設置定時器。你還知道如何通過將定時器的句柄傳遞給 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函數,以便等待定時器報時。M i c r o s o f t還允許定時器給在定時器得到通知信號時調用 S e t Wa i t a b l e Ti m e r函數的線程的異步過程調用(A P C)進行排隊。
? ? 一般來說,當調用 S e t Wa i t a b l e Ti m e r函數時,你將同時為 p f n C o m p l e t i o n R o u t i n e和p v A rg C o m p l e t i o n R o u t i n e參數傳遞N U L L。當S e t Wa i t a b l e Ti m e函數看到這些參數的N U L L時,它就知道,當規定的時間到來時,就向定時器發出通知信號。但是,如果到了規定的時間,你愿意讓定時器給一個A P C排隊,那么你必須傳遞定時器A P C例程的地址,而這個例程是你必須實現的。該函數應該類似下面的形式:
? ? 我已經將該函數命名為Ti m e r A P C R o u t i n e,不過可以根據需要給它賦予任何一個名字。該函數可以在定時器報時的時候由調用S e t Wa i t a b l e Ti m e r函數的同一個線程來調用,但是只有在調用線程處于待命狀態下才能調用。換句話說,該線程必須正在 S l e e p E x , Wa i t F o r S n g l e O b j e c t E x,Wa i t F o r M u l t i p l e O b j e c t s E x,M s g Wa i t F o r M u l t i p l e O b j e c t s E x或S i n g l e O b j e c t - A n d Wa i t等函數的調用中等待。如果該線程不在這些函數中的某個函數中等待,系統將不給定時器 A P C例程排隊。這可以防止線程的A P C隊列中塞滿定時器A P C通知,這會浪費系統中的大量內存。
? ? 當定時器報時的時候,如果你的線程處于待命的等待狀態中,系統就使你的線程調用回調例程。回調例程的第一個參數的值與你傳遞給S e t Wa i t a b l e Ti m e r函數的p v A rg To C o m p l e t i o n R o u t i n e參數的值是相同的。你可以將某些上下文信息(通常是你定義的某個結構的指針)傳遞給Ti m e r A P C R o u t i n e。剩余的兩個參數d w Ti m e r L o w Va l u e和d w Ti m e r H i g h Va l u e用于指明定時器何時報時。下面的代碼使用了該信息,并將它顯示給用戶:
????只有當所有的A P C項都已經處理之后,待命的函數才會返回。因此,必須確保定時器再次變為已通知狀態之前,Ti m e r A P C R o u t i n e函數完成它的運行,這樣,A P C項的排隊速度就不會比它被處理的速度快。
下面的代碼顯示了使用定時器和A P C項的正確方法:
? ? 最后要說明的是,線程不應該等待定時器的句柄,也不應該以待命的方式等待定時器。請看下面的代碼:
?
? ? 不應該編寫上面的代碼,因為調用Wa i t F o r S i n g l e O b j e c t E x函數實際上是兩次等待該定時器,一次是以待命方式等待,一次是等待內核對象句柄。當定時器變為已通知狀態時,等待就成功了,線程被喚醒,這將使該線程擺脫待命狀態,而 A P C例程則沒有被調用。前面講過,通常沒有理由使用帶有等待定時器的A P C例程,因為你始終都可以等待定時器變為已通知狀態,然后做你想要做的事情。
9.4.2 定時器的松散特性
? ? 定時器常常用于通信協議中。例如,如果客戶機向服務器發出一個請求,而服務器沒有在規定的時間內作出響應,那么客戶機就會認為無法使用服務器。目前,客戶機通常要同時與許多服務器進行通信。如果你為每個請求創建一個定時器內核對象,那么系統的運行性能就會受到影響。可以設想,對于大多數應用程序來說,可以創建單個定時器對象,并根據需要修改定時器報時的時間。
? ? 定時器報時時間的管理方法和定時器時間的重新設定是非常麻煩的,只有很少的應用程序采用這種方法。但是在新的線程共享函數(第 11 章中介紹)中有一個新函數,稱為C r e a t e Ti m e r Q u e u e Ti m e r,它能夠為你處理所有的操作。如果你發現自己創建和管理了若干個定時器對象,那么應該觀察一下這個函數,以減少應用程序的開銷。
9.5 信標內核對象
? ? 信標內核對象用于對資源進行計數。它們與所有內核對象一樣,包含一個使用數量,但是它們也包含另外兩個帶符號的3 2位值,一個是最大資源數量,一個是當前資源數量。最大資源數量用于標識信標能夠控制的資源的最大數量,而當前資源數量則用于標識當前可以使用的資源的數量。
? ? 為了正確地說明這個問題,讓我們來看一看應用程序是如何使用信標的。比如說,我正在開發一個服務器進程,在這個進程中,我已經分配了一個能夠用來存放客戶機請求的緩沖區。我對緩沖區的大小進行了硬編碼,這樣它每次最多能夠存放 5個客戶機請求。如果5個請求尚未處理完畢時,一個新客戶機試圖與服務器進行聯系,那么這個新客戶機的請求就會被拒絕,并出現一個錯誤,指明服務器現在很忙,客戶機應該過些時候重新進行聯系。當我的服務器進程初始化時,它創建一個線程池,里面包含 5個線程,每個線程都準備在客戶機請求到來時對它進行處理。
? ? 開始時,沒有客戶機提出任何請求,因此我的服務器不允許線程池中的任何線程成為可調度線程。但是,如果3個客戶機請求同時到來,那么線程池中應該有 3個線程處于可調度狀態。使用信標,就能夠很好地處理對資源的監控和對線程的調度,最大資源數量設置為 5,因為這是我進行硬編碼的緩沖區的大小。當前資源數量最初設置為 0,因為沒有客戶機提出任何請求。當客戶機的請求被接受時,當前資源數量就遞增,當客戶機的請求被提交給服務器的線程池時,當前資源數量就遞減。
? ? 信標的使用規則如下:
? ? ? 如果當前資源的數量大于0,則發出信標信號。
? ? ? 如果當前資源數量是0,則不發出信標信號。
? ? ? 系統決不允許當前資源的數量為負值。
? ? ? 當前資源數量決不能大于最大資源數量。
? ? 當使用信標時,不要將信標對象的使用數量與它的當前資源數量混為一談。
? ? 下面的函數用于創建信標內核對象:
HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTE psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName
);
? ? p s a和p s z N a m e兩個參數在第3章中作過介紹。當然,通過調用 O p e n S e m a p h o r e函數,另一個進程可以獲得它自己的進程與現有信標相關的句柄:
? ? l M a x i m u m C o u n t參數用于告訴系統,應用程序處理的最大資源數量是多少。由于這是個帶符號的3 2位值,因 此最多可以擁有2 147 483 647個資源。l I n i t i a l C o u n t參數用于指明開始時(當前)這些資源中有多少可供使用。當我的服務器進程初始化時,沒有任何客戶機請求,因此我調用下面這個C r e a t e S e m a p h o r e函數:
HANDLE hsem = CreateSemaphore(NULL ,0 ,5 ,NULL);
? ? 該函數創建了一個信標,其最大資源數量為 5,但是開始時可以使用的資源為0(由于偶然的原因,該內核對象的使用數量是1,因為我剛剛創建了這個內核對象,請不要與計數器搞混) 。由于當前資源數量被初始化為0,因此不發出信標信號。等待信標的所有線程均進入等待狀態。
? ? 通過調用等待函數,傳遞負責保護資源的信標的句柄,線程就能夠獲得對該資源的訪問權。從內部來說,該等待函數要檢查信標的當前資源數量,如果它的值大于0(信標已經發出信號) ,那么計數器遞減1,調用線程保持可調度狀態。信標的出色之處在于它們能夠以原子操作方式來執行測試和設置操作,這就是說,當向信標申請一個資源時,操作系統就要檢查是否有這個資源可供使用,同時將可用資源的數量遞減,而不讓另一個線程加以干擾。只有當資源數量遞減后,系統才允許另一個線程申請對資源的訪問權。
? ? 如果該等待函數確定信標的當前資源數量是 0(信標沒有發出通知信號) ,那么系統就調用函數進入等待狀態。當另一個線程將對信標的當前資源數量進行遞增時,系統會記住該等待線程(或多個線程) ,并允許它變為可調度狀態(相應地遞減它的當前資源數量) 。
? ? 通過調用R e l e a s e S e m a p h o r e函數,線程就能夠對信標的當前資源數量進行遞增:
BOOL ReleaseSemaphore(
????HANDLE hsem,
????LONG lReleaseCount,
????PLONG plPreviousCount
);
? ? 該函數只是將 l R e l e a s e C o u n t中的值添加給信標的當前資源數量。通常情況下,為l R e l e a s e C o u n t參數傳遞1,但是,不一定非要傳遞這個值。我常常傳遞 2或更大的值。該函數也能夠在它的* p l P r e v i o u s C o u n t中返回當前資源數量的原始值。實際上幾乎沒有應用程序關心這個值,因此可以傳遞N U L L,將它忽略。
? ? 有時,有必要知道信標的當前資源數量而不修改這個數量,但是沒有一個函數可以用來查詢信標的當前資源數量的值。起先我認為調用 R e l e a s e S e m a p h o r e并為l R e l e a s e C o u n t參數傳遞0,也許會在* p l P r e v i o u s C o u n t中返回資源的實際數量。但是這樣做是不行的,R e l e a s e S e m a p h o r e用0填入這個長變量。接著,我試圖傳遞一個非常大的數字,作為第二個參數,希望它不會影響當前資源數量,因為它將取代最大值。同樣,R e l e a s e S e m a p h o r e用0填入* p l P r e v i o u s。可惜,如果不對它進行修改,就沒有辦法得到信標的當前資源數量。
9.6 互斥對象內核對象
? ? 互斥對象(m u t e x)內核對象能夠確保線程擁有對單個資源的互斥訪問權。實際上互斥對象是因此而得名的。互斥對象包含一個使用數量,一個線程 I D和一個遞歸計數器。互斥對象的行為特性與關鍵代碼段相同,但是互斥對象屬于內核對象,而關鍵代碼段則屬于用戶方式對象。這意味著互斥對象的運行速度比關鍵代碼段要慢。但是這也意味著不同進程中的多個線程能夠訪問單個互斥對象,并且這意味著線程在等待訪問資源時可以設定一個超時值。
? ? I D用于標識系統中的哪個線程當前擁有互斥對象,遞歸計數器用于指明該線程擁有互斥對象的次數。互斥對象有許多用途,屬于最常用的內核對象之一。通常來說,它們用于保護由多個線程訪問的內存塊。如果多個線程要同時訪問內存塊,內存塊中的數據就可能遭到破壞。互斥對象能夠保證訪問內存塊的任何線程擁有對該內存塊的獨占訪問權,這樣就能夠保證數據的完整性。
? ? 互斥對象的使用規則如下:
? ? ? 如果線程I D是0(這是個無效I D) ,互斥對象不被任何線程所擁有,并且發出該互斥對象
的通知信號。
? ? ? 如果I D是個非0數字,那么一個線程就擁有互斥對象,并且不發出該互斥對象的通知信
號。
? ? ? 與所有其他內核對象不同, 互斥對象在操作系統中擁有特殊的代碼,允許它們違反正常
的規則(后面將要介紹這個異常情況) 。
? ? 若要使用互斥對象,必須有一個進程首先調用C r e a t e M u t e x,以便創建互斥對象:
HANDLE CreateMutex(
????PSECURITY_ATTRIBUTES psa,
????BOOL fInitialOwner,
????PCTSTR pszName);
? ? p s a和p s z N a m e參數在第3章中做過介紹。當然,通過調用 O p e n M u t e x,另一個進程可以獲得它自己進程與現有互斥對象相關的句柄:
HANDLE OpenMutex(
????DWORD fdwAccess,
????BOOL bInheritHandle,
????PCTSTR pszName
);
? ? f I n i t i a l O w n e r參數用于控制互斥對象的初始狀態。如果傳遞 FA L S E(這是通常情況下傳遞的值) ,那么互斥對象的I D和遞歸計數器均被設置為0。這意味著該互斥對象沒有被任何線程所擁有,因此要發出它的通知信號。
? ? 如果為f I n i t i a l O w n e r參數傳遞T R U E,那么該對象的線程I D被設置為調用線程的I D,遞歸計數器被設置為1。由于I D是個非0數字,因此該互斥對象開始時不發出通知信號。
? ?通過調用一個等待函數,并傳遞負責保護資源的互斥對象的句柄,線程就能夠獲得對共享資源的訪問權。在內部,等待函數要檢查線程的 I D,以了解它是否是0(互斥對象發出通知信號) 。如果線程I D是0,那么該線程I D被設置為調用線程的I D,遞歸計數器被設置為 1,同時,調用線程保持可調度狀態。
? ? 如果等待函數發現I D不是0(不發出互斥對象的通知信號) ,那么調用線程便進入等待狀態。
系統將記住這個情況,并且在互斥對象的 I D重新設置為0時,將線程I D設置為等待線程的I D,將遞歸計數器設置為1,并且允許等待線程再次成為可調度線程。與所有情況一樣,對互斥內核對象進行的檢查和修改都是以原子操作方式進行的。
? ? 對于互斥對象來說,正常的內核對象的已通知和未通知規則存在一個特殊的異常情況。比如說,一個線程試圖等待一個未通知的互斥對象。在這種情況下,該線程通常被置于等待狀態。然而,系統要查看試圖獲取互斥對象的線程的I D是否與互斥對象中記錄的線程I D相同。如果兩個線程I D相同,即使互斥對象處于未通知狀態,系統也允許該線程保持可調度狀態。我們不認為該“異常”行為特性適用于系統中的任何地方的其他內核對象。每當線程成功地等待互斥對象時,該對象的遞歸計數器就遞增。若要使遞歸計數器的值大于 1,唯一的方法是線程多次等待相同的互斥對象,以便利用這個異常規則。
? ? 一旦線程成功地等待到一個互斥對象,該線程就知道它已經擁有對受保護資源的獨占訪問權。試圖訪問該資源的任何其他線程(通過等待相同的互斥對象)均被置于等待狀態中。當目前擁有對資源的訪問權的線程不再需要它的訪問權時,它必須調用 R e l e a s e M u t e x函數來釋放該互斥對象:
BOOL ReleaseMutex(HANDLE hMutex);
? ? 該函數將對象的遞歸計數器遞減 1。如果線程多次成功地等待一個互斥對象,在互斥對象的遞歸計數器變成0之前,該線程必須以同樣的次數調用 R e l e a s e M u t e x函數。當遞歸計數器到達0時,該線程I D也被置為0,同時該對象變為已通知狀態。
? ? 當該對象變為已通知狀態時,系統要查看是否有任何線程正在等待互斥對象。如果有,系統將“按公平原則”選定等待線程中的一個,為它賦予互斥對象的所有權。當然,這意味著線程I D被設置為選定的線程的I D,并且遞歸計數器被置為1。如果沒有其他線程正在等待互斥對象,那么該互斥對象將保持已通知狀態,這樣,等待互斥對象的下一個線程就立即可以得到互斥對象。
9.6.1 釋放問題
? ? 互斥對象不同于所有其他內核對象,因為互斥對象有一個“線程所有權”的概念。本章介紹的其他內核對象中,沒有一種對象能夠記住哪個線程成功地等待到該對象,只有互斥對象能夠對此保持跟蹤。互斥對象的線程所有權概念是互斥對象為什么會擁有特殊異常規則的原因,這個異常規則使得線程能夠獲取該互斥對象,盡管它沒有發出通知。
? ? 這個異常規則不僅適用于試圖獲取互斥對象的線程, 而且適用于試圖釋放互斥對象的線程。當一個線程調用R e l e a s e M u t e x函數時,該函數要查看調用線程的I D是否與互斥對象中的線程I D相匹配。如果兩個I D相匹配,遞歸計數器就會像前面介紹的那樣遞減。如果兩個線程的 I D不匹配,那么R e l e a s e M u t e x函數將不進行任何操作,而是將FA L S E(表示失敗)返回給調用者。此
時調用G e t L a s t E r r o r,將返回E R R O R _ N O T _ O W N E R(試圖釋放不是調用者擁有的互斥對象) 。
? ? 因此,如果在釋放互斥對象之前,擁有互斥對象的線程終止運行(使用 E x i t T h r e a d、Te r m i n a t e T h r e a d、E x i t P r o c e s s或Te r m i n a t e P r o c e s s函數) ,那么互斥對象和正在等待互斥對象的其他線程將會發生什么情況呢?答案是,系統將把該互斥對象視為已經被放棄——擁有互斥對象的線程決不會釋放它,因為該線程已經終止運行.
? ? 由于系統保持對所有互斥對象和線程內核對象的跟蹤,因此它能準確的知道互斥對象何時被放棄。當一個互斥對象被放棄時,系統將自動把互斥對象的 I D復置為0,并將它的遞歸計數器復置為0。然后,系統要查看目前是否有任何線程正在等待該互斥對象。如果有,系統將“公平地”選定一個等待線程,將 I D設置為選定的線程的I D,并將遞歸計數器設置為1,同時,選定的線程變為可調度線程。
? ? 這與前面的情況相同,差別在于等待函數并不將通常的 WA I T _ O B J E C T _ 0值返回給線程。相反,等待函數返回的是特殊的 WA I T _ A B A N D O N E D值。這個特殊的返回值(它只適用于互斥對象)用于指明線程正在等待的互斥對象是由另一個線程擁有的,而這另一個線程已經在它完成對共享資源的使用前終止運行。這不是可以進入的最佳情況。新調度的線程不知道目前資源處于何種狀態,也許該資源已經完全被破壞了。在這種情況下必須自己決定應用程序應該怎么辦。
? ? 在實際運行環境中,大多數應用程序從不明確檢查 WA I T _ A B A N D O N E D返回值,因為線程很少是剛剛終止運行(上面介紹的情況提供了另一個例子,說明為什么決不應該調用Te r m i n a t e T h r e a d函數) 。
9.6.2 互斥對象與關鍵代碼段的比較
? ? 就等待線程的調度而言,互斥對象與關鍵代碼段之間有著相同的特性。但是它們在其他屬性方面卻各不相同。表9 - 1對它們進行了各方面的比較。
?
9.7 線程同步對象速查表
表9 - 2所示的速查表綜合列出了各種內核對象與線程同步之間的相互關系。
?
?
?
? ? 互鎖(用戶方式)函數決不會導致線程變為非調度狀態,它們會改變一個值并立即返回。
9.8 其他的線程同步函數
? ? 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是進行線程同步時使用得最多的函數。但是,Wi n d o w s還提供了另外幾個稍有不同的函數。如果理解了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 eO b j e c t s函數,那么要理解其他函數如何運行,就不會遇到什么困難。本節簡單地介紹一些這樣的函數。
9.8.1 異步設備I / O
? ? 異步設備I / O使得線程能夠啟動一個讀操作或寫操作,但是不必等待讀操作或寫操作完成。例如,如果線程需要將一個大文件裝入內存,那么該線程可以告訴系統將文件裝入內存。然后,當系統加載該文件時,該線程可以忙于執行其他任務,如創建窗口、對內部數據結構進行初始化等等。當初始化操作完成時,該線程可以終止自己的運行,等待系統通知它文件已經讀取。
? ? 設備對象是可以同步的內核對象,這意味著可以調用 Wa i t F o r S i n g l e O b j e c t函數,傳遞文件、套接字和通信端口的句柄。當系統執行異步 I / O時,設備對象處于未通知狀態。一旦操作完成,系統就將對象的狀態改為已通知狀態,這樣,該線程就知道操作已經完成。此時,該線程就可以繼續運行。
9.8.2 Wa i t F o r I n p u t I d l e
? ? 線程也可以調用Wa i t F o r I n p u t I d l e來終止自己的運行:
?
? ? 該函數將一直處于等待狀態,直到 h P r o c e s s標識的進程在創建應用程序的第一個窗口的線程中已經沒有尚未處理的輸入為止。這個函數可以用于父進程。父進程產生子進程,以便執行某些操作。當父進程的線程調用C r e a t e P r o c e s s時,該父進程的線程將在子進程初始化時繼續運行。父進程的線程可能需要獲得子進程創建的窗口的句柄。如果父進程的線程想要知道子進程何時完成初始化,唯一的辦法是等待,直到子進程不再處理任何輸入為止。因此,當調用C r e a t e P r o c e s s后,父進程的線程就調用Wa i t F o r I n p u t I d l e。
當需要將擊鍵輸入納入應用程序時,也可以調用 Wa i t F o r I n p u t I d l e。比如說,可以將下面的消息顯示在應用程序的主窗口:
?
? ? 這個序列將A l t + F, O發送給應用程序,對于大多數使用英語的應用程序來說,它從應用程序的文件菜單中選擇 O p e n命令。該命令打開一個對話框,但是,在對話框出現以前,Wi n d o w s必須加載來自文件的對話框摸板,遍歷摸板中的所有控件,并為每個摸板調用 C r e a t eWi n d o w。這可能需要花費一定的時間。因此,顯示 W M _ K E Y *消息的應用程序可以調用Wa i t F o r I n p u t I d l e,Wa i t F o r l n p u tId l e將導致應用程序處于等待狀態,直到對話框創建完成并準備接受用戶的輸入。這時,該應用程序可以將其他的擊鍵輸入納入對話框及其控件,使它能夠繼續執行它需要的操作。
? ? 編寫1 6位Wi n d o w s應用程序的編程人員常常要面對這個問題。應用程序想要將消息顯示在窗口中,但是它并不確切知道窗口何時創建完成、作好接受消息的準備。 Wa i t F o r I n p u t I d l e函數解決了這個問題。
9.8.3 MsgWa i t F o r M u l t i p l e O b j e c t s ( E x )
????線程可以調用M s g Wa i t F o r M u l t i p l e O b j e c t s或M s g Wa i t F o r M u l t i p l e O b j e c t s E x函數,讓線程等
待它自己的消息:
?
? ? 這些函數與Wa i t F o r M u l t i p l e O b j e c t s函數十分相似。差別在于它們允許線程在內核對象變成已通知狀態或窗口消息需要調度到調用線程創建的窗口中時被調度。
? ? 創建窗口和執行與用戶界面相關的任務的線程,應該調用 M s g Wa i t F o r M u l t i p l e O b j e c t s E x函數,而不應該調用Wa i t F o r M u l t i p l e O b j e c t s函數,因為后面這個函數將使線程的用戶界面無法對用戶作出響應。該函數將在第2 7章中詳細介紹。
9.8.4 Wa i t F o r D e b u g E v e n t
? ? Wi n d o w s將非常出色的調試支持特性內置于操作系統之中。當調試程序啟動運行時,它將自己附加給一個被調試程序。該調試程序只需閑置著,等待操作系統將與被調試程序相關的調試事件通知它。調試程序通過調用Wa i t F o r D e b u g E v e n t函數來等待這些事件的發生:
?
? ? 當調試程序調用該函數時,調試程序的線程終止運行,系統將調試事件已經發生的情況通知調試程序,方法是允許調用的Wa i t F o r D e b u g E v e n t函數返回。p d e參數指向的結構在喚醒調試程序的線程之前由系統填入信息。該結構包含了關于剛剛發生的調試事件的信息。
9.8.5 SingleObjectAndWa i t
? ? S i n g l e O b j e c t A n d Wa i t函數用于在單個原子方式的操作中發出關于內核對象的通知并等待另一個內核對象:
?
? ? 當調用該函數時,h O b j e c t To S i g n a l參數必須標識一個互斥對象、信標對象或事件。任何其他 類型的 對象將 導致 該函數 返回 WA I T _ FA I L E D ,并使 G e t L a s t E r r o r函 數返 回E R R O R _ I N VA L I D _ H A N D L E。在內部,該函數將觀察對象的類型,并分別運行 R e l e a s e M u t e x、R e l e a s e S e m a p h o r e (其數量為1) 或R e s e t E v e n t中的相應參數。
? ? h O b j e c t To Wa i t O n參數用于標識下列任何一個內核對象:互斥對象、信標、事件、定時器、進程、線程、作業、控制臺輸入和修改通知。與平常一樣, d w M i l l i s e c o n d s參數指明該函數為了等待該對象變為已通知狀態,應該等待多長時間,而 f A l e r t a b l e標志則指明線程等待時該線程是否應該能夠處理任何已經排隊的異步過程調用。
? ? 該函數返回下列幾個值中的一個: 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 _ A B A N D O N E D(本章前面已經介紹)或WA I T _ I O _ C O M P L E T I O N。
? ? 該函數是對Wi n d o w s的令人歡迎的一個補充,原因有二。首先,因為常常需要通知一個對象,等待另一個對象,用一個函數來執行兩個操作可以節省許多處理時間。每次調用一個函數,使線程從用戶方式代碼變成內核方式代碼,大約需要運行 1 0 0 0個C P U周期。例如,運行下面的代碼至少需要2 0 0 0個C P U周期:
ReleaseMutex(hMutex);
WaitForSingleObject(hEvent ,INFINITE);
? ? 在高性能服務器應用程序中,S i g n a l O b j e c t A n d Wa i t函數能夠節省大量的處理時間。
? ? 第二,如果沒有S i g n a l O b j e c t A n d Wa i t函數,一個線程將無法知道另一個線程何時處于等待狀態。對于 P l u s e E v e n t之類的函數來說,知道這個情況是很有用的。本章前面講過,P u l s e E v e n t函數能夠通知一個事件,并且立即對它進行重置。如果目前沒有任何線程等待該事件,那么就沒有事件會抓住這個情況。曾經有人編寫過類似下面的代碼:
?
? ? 一個工作線程負責運行一些代碼,然后調用 S e t E v e n t,以指明這項工作已經完成。另一個線程負責執行下面的代碼:
?
? ? 這個工作線程的代碼段設計得很差,因為它無法可靠地運行。當工作線程調用 S e t E v e n t之后,另一個線程可能立即醒來,并調用 P u l s e E v e n t。該工作線程不得不停止運行,沒有機會從它對S e t E v e n t的調用中返回,更不要說調用 Wa i t F o r S i n g l e O b j e c t函數了。結果, h E v e n tM o r e Wo r k To B e D o n e事件的通知就完全被工作線程錯過了。
? ? 如果像下面所示的那樣重新編寫工作線程的代碼,以便調用 S i n g l e O b j e c t A n d Wa i t函數,那么該代碼就能夠可靠地運行,因為通知和等待都能夠以原子操作方式來進行:
?
? ? 當非工作線程醒來時,它能夠百分之百地確定工作線程正在等待 h E v e n t M o r e Wo r k To B eD o n e事件,因此能夠確保看到產生該事件。
? ? Windows 98 Windows 98 沒有這個函數的可以使用的實現代碼。
總結
以上是生活随笔為你收集整理的Windows核心编程 第九章 线程与内核对象的同步(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows核心编程 第九章 线程与内
- 下一篇: Windows核心编程 第十一章 线程池