Windows核心编程 第八章 用户方式中线程的同步(下)
8.4 關鍵代碼段
? ? 關鍵代碼段是指一個小代碼段,在代碼能夠執行前,它必須獨占對某些共享資源的訪問權。這是讓若干行代碼能夠“以原子操作方式”來使用資源的一種方法。所謂原子操作方式,是指該代碼知道沒有別的線程要訪問該資源。當然,系統仍然能夠抑制你的線程的運行,而搶先安排其他線程的運行。不過,在線程退出關鍵代碼段之前,系統將不給想要訪問相同資源的其他任何線程進行調度。
? ? 下面是個有問題的代碼,它顯示了不使用關鍵代碼段會發生什么情況:
?
? ? 如果分開來看,這兩個線程函數將會產生相同的結果,不過每個函數的編碼略有不同。如果F i r s t T h r e a d函數自行運行,它將用遞增的值填入 g _ d w Ti m e s數組。如果S e c o n d T h r e a d函數也是自行運行,那么情況也一樣。在理想的情況下,我們希望兩個線程能夠同時運行,并且仍然使g _ d w Ti m e s數組能夠產生遞增的值。但是,上面的代碼存在一個問題,那就是 g _ d w Ti m e s不會被正確地填入數據,因為兩個線程函數要同時訪問相同的全局變量。
? ? 下面是如何出現這種情況的一個例子。比如說,我們剛剛在只有一個 C P U的系統上啟動執行兩個線程。操作系統首先啟動運行 S e c o n d T h r e a d(這種情況很可能出現) ,當S e c o n d T h r e a d將g _ n I n d e x遞增為1之后,系統就停止該線程的運行,而讓F i r s t T h r e a d運行。這時F i r s t T h r e a d將g _ d w Ti m e s [ 1 ]設置為系統時間,然后系統停止該線程的運行,將 C P U時間重新賦予S e c o n d T h r e a d線程。然后S e c o n d T h r e a d將g _ d w Times[1 -1 ]設置為新的系統時間。由于這個操作發生在較晚的時間,因此新系統時間的值大于放入F i r s t T h r e a d數組中的時間值,另外要注意,g _ d w Ti m e s的索引1填在索引0的前面。數組中的數據被破壞了。
? ? 應該說明的是,這個例子的設計帶有一定的故意性,因為要設計一個實際工作中的例子而不使用好幾頁的源代碼是很難的。不過,通過這個例子,能夠看到這個問題在實際工作中有些什么表現。考慮一下管理一個鏈接對象列表的情況。如果對該鏈接列表的訪問沒有取得同步,那么一個線程可以將一個項目添加給這個列表, 而另一個線程則試圖搜索該列表中的一個項目。如果兩個線程同時給這個列表添加項目,那么這種情況會變得更加復雜。通過運用關鍵代碼段,就能夠確保在各個線程之間協調對數據結構的訪問。
既然已經了解了存在的所有問題,那么下面讓我們用關鍵代碼段來修正這個代碼:
? ? 這里指定了一個C R I T I C A L _ S E C T I O N數據結構g _ c s,然后在對E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l e c t i o n函數調用中封裝了要接觸共享資源(在這個例子中為g _ n I n d e x和g _ d w Ti m e s)的任何代碼。注意,在對E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n的所有調用中,我傳遞了g _ c s的地址。
? ? 注意 最難記住的一件事情是,編寫的需要使用共享資源的任何代碼都必須封裝在E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n函數中。如果忘記將代碼封裝在一個位置,共享資源就可能遭到破壞。例如,如果我刪除了F r i s t T h r e a d線程對E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n的調用, g _ n I n d e x和g _ d w Ti m e s變量就會遭到破壞。即使S e c o n d T h r e a d線程仍然正確地調用E n t e r C r i t i c a l S e c t i o n和L e a v e C r i t i c a l S e c t i o n,也會出現這種情況。
? ? ?當無法用互鎖函數來解決同步問題時,你應該試用關鍵代碼段。關鍵代碼段的優點在于它們的使用非常容易,它們在內部使用互鎖函數,這樣它們就能夠迅速運行。關鍵代碼的主要缺點是無法用它們對多個進程中的各個線程進行同步。不過在第 1 9章中,我將要創建我自己的同步對象,稱為O p t e x。這個對象將顯示操作系統如何來實現關鍵代碼段,它也能用于多個進程中的各個線程。
8.4.1 關鍵代碼段準確的描述
? ? 現在你已經從理論上對關鍵代碼段有了一定的了解。已經知道為什么它們非常有用,以及它們是如何實現“以原子操作方式”對共享資源進行訪問的。下面讓我們更加深入地看一看關鍵代碼段是如何運行的。首先介紹一下 C R I T I C A L _ S E C T I O N數據結構。如果想查看一下Platform SDK文檔中關于該結構的說明,也許你會感到無從下手。那么問題究竟何在呢?
? ? 并不是C R I T I C A L _ S E C T I O N結構沒有完整的文檔,而是 M i c r o s o f t認為沒有必要了解該結構的全部情況,這是對的。對于我們來說,這個結構是透明的,該結構有文檔可查,但是該結構中的成員變量沒有文檔。當然,由于這只是個數據結構,可以在 Wi n d o w s頭文件中查找這些信息,可以看到這些數據成員( C R I T I C A L _ S E C T I O N在Wi n N T. h中定義為RT L _ C R I T I C A L _S E C T I O N;RT L _ C R I T I C A L _ S E C T I O N結構在Wi n B a s e . h中作了定義) 。但是決不應該編寫引用這些成員的代碼。
? ? 若要使用C R I T I C A L _ S E C T I O N結構,可以調用一個Wi n d o w s函數,給它傳遞該結構的地址。該函數知道如何對該結構的成員進行操作,并保證該結構的狀態始終一致。因此下面讓我們將注意力轉到這些函數上去。
通常情況下,C R I T I C A L _ S E C T I O N結構可以作為全局變量來分配,這樣,進程中的所有線程就能夠很容易地按照變量名來引用該結構。但是, C R I T I C A L _ S E C T I O N結構也可以作為局部變量來分配,或者從堆棧動態地進行分配。它只有兩個要求,第一個要求是,需要訪問該資源的所有線程都必須知道負責保護資源的 C R I T I C A L _ S E C T I O N結構的地址,你可以使用你喜歡的任何機制來獲得這些線程的這個地址;第二個要求是, C R I T I C A L _ S E C T I O N結構中的成員應該在任何線程試圖訪問被保護的資源之前初始化。該結構通過調用下面的函數來進行初始化:
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
? ? 該函數用于對(p c s指向的)C R I T I C A L _ S E C T I O N結構的各個成員進行初始化。由于該函數只是設置了某些成員變量。因此它的運行不會失敗,并且它的原型采用了 V O I D的返回值。該函數必須在任何線程調用 E n t e r C r i t i c a l S e c t i o n函數之前被調用。Platform SDK的文檔清楚地說明,如果一個線程試圖進入一個未初始化的 C RT I C A L _ S E C T I O N,那么結果將是很難預計的。
? ? 當知道進程的線程不再試圖訪問共享資源時,應該通過調用下面的函數來清除該C R I T I C A L _ S E C T I O N結構:
VOID DeleteCriticalSection(PCRITICAL_SECTION pcs);
?
? ? 如果E n t e r C r i t i c a l S e c t i o n將一個線程置于等待狀態,那么該線程在很長時間內就不能再次被調度。實際上,在編寫得不好的應用程序中,該線程永遠不會再次被賦予 C P U時間。如果出現這種情況,該線程就稱為渴求C P U時間的線程。
? ? Windows 2000 在實際操作中,等待關鍵代碼段的線程絕對不會渴求 C P U時間。對E n t e r C r i t i c a l S e c t i o m的調用最終將會超時,導致產生一個異常條件。這時可以將一個調試程序附加給應用程序,以確定究竟出了什么問題。超時的時間量是由下面的注冊表子關鍵字中包含的C r i t i c a l S e c t i o n Ti m e o u t數據值來決定的。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager
我的win7 64是這樣
?
? ? 這個值以秒為單位,默認為 2 592 000s,即大約3 0天。不要將這個值設置得太小(比如小于3 s) ,否則就會對系統中正常等待關鍵代碼段超過 3 s的線程和其他應用程序產生不利的影響。
可以使用下面這個函數來代替E n t e r C r i t i c a l S e c t i o n:
?
? ? Tr y E n t e r C r i t i c a l S e c t i o n函數決不允許調用線程進入等待狀態。相反,它的返回值能夠指明調用線程是否能夠獲得對資源的訪問權。因此,如果 Tr y E n t e r C r i t i c a l S e c t i o n發現該資源已經被另一個線程訪問,它就返回FA L S E。在其他所有情況下,它均返回T R U E。
? ? 運用這個函數,線程能夠迅速查看它是否可以訪問某個共享資源,如果不能訪問,那么它可以繼續執行某些其他操作,而不必進行等待。如果 Tr y E n t e r C r i t i c a l S e c t i o n函數確實返回了T R U E,那么C R I T I C A L _ S E C T I O N的成員變量已經更新,以便反映出該線程正在訪問該資源。因此,對返回T R U E的Tr y E n t e r C r i t i c a l S e c t i o n函數的每次調用都必須與對L e a v e C r i t i c a l S e c t i o n函數的調用相匹配。
8.4.2 關鍵代碼段與循環鎖
? ? 當線程試圖進入另一個線程擁有的關鍵代碼段時,調用線程就立即被置于等待狀態。這意味著該線程必須從用戶方式轉入內核方式(大約 1 0 0 0個C P U周期) 。這種轉換是要付出很大代價的。在多處理器計算機上,當前擁有資源的線程可以在不同的處理器上運行,并且能夠很快放棄對資源的控制。 實際上擁有資源的線程可以在另一個線程完成轉入內核方式之前釋放資源。如果出現這種情況,就會浪費許多C P U時間。
? ? 為了提高關鍵代碼段的運行性能, M i c r o s o f t將循環鎖納入了這些代碼段。因此,當E n t e r C r i t i c a l S e c t i o n函數被調用時,它就使用循環鎖進行循環,以便設法多次取得該資源。只有當為了取得該資源的每次試圖都失敗時,該線程才轉入內核方式,以便進入等待狀態。
若要將循環鎖用于關鍵代碼段,應該調用下面的函數,以便對關鍵代碼段進行初始化:
?
? ? 與I n i t i a l i z e C r i t i c a l S e c t i o n中的情況一樣,I n i t i a l i z e C r i t i c a l S e c t i o n A n d S p i n C o u n t的第一個參數是關鍵代碼段結構的地址。但是在第二個參數 d w S p i n C o u n t中,傳遞的是在使線程等待之前它試圖獲得資源時想要循環鎖循環迭代的次數。 這個值可以是0至0 x 0 0 F F F F F F之間的任何數字。如果在單處理器計算機上運行時調用該函數, d w S p i n C o u n t參數將被忽略,它的計數始終被置為0。這是對的,因為在單處理器計算機上設置循環次數是毫無用處的,如果另一個線程正在循環運行,那么擁有資源的線程就不能放棄它。
通過調用下面的函數,就能改變關鍵代碼段的循環次數:
?
? ? 同樣,如果主計算機只有一個處理器,那么 d w S p i n C o u n t的值將被忽略。我認為,始終都應該將循環鎖用于關鍵代碼段,因為這樣做有百利而無一害。難就難在確定為 d w S p i n C o u n t參數傳遞什么值。為了實現最佳的性能,只需要調整這些數字,直到對性能結果滿意為止。作為一個指導原則,保護對進程的堆棧進行訪問的關鍵代碼段使用的循環次數是 4 0 0 0次。
?
8 .4.3 關鍵代碼段與錯誤處理
? ? I n i t i a l i z e C r i t i c a l S e c t i o n函數的運行可能失敗(盡管可能性很小) 。M i c r o s o f t在最初設計該函數時并沒有真正想到這個問題,正因為這個原因,該函數的原型才設計為返回 V O I D。該函數的運行可能失敗,因為它分配了一個內存塊以便系統得到一些內部調試信息。如果該內存的分配失敗,就會出現一個S TAT U S _ N O _ M E M O RY異常情況。可以使用結構化異常處理(第2 3、2 4和2 5章介紹)來跟蹤代碼中的這種異常情況。
? ? 使用更新的I n i t i a l i z e C r i t i c a l S e c t i o n A n d S p i n C o u n t函數,就能夠更加容易地跟蹤這個問題。該函數也為調試信息分配了內存塊,如果內存無法分配,那么它就返回 FA L S E。
? ? 當使用關鍵代碼段時還會出現另一個問題。從內部來說,如果兩個或多個線程同時爭用關鍵代碼段,那么關鍵代碼段將使用一個事件內核對象(第1 0章介紹Coptex C++類時,我將要說明如何使用該內核對象) 。由于爭用的情況很少發生,因此,在初次需要之前,系統將不創建事件內核對象。這可以節省大量的系統資源,因為大多數關鍵代碼段從來不被爭用。
? ? 在內存不足的情況下,關鍵代碼段可能被爭用,同時系統可能無法創建必要的事件內核對象。這時E n t e r C r i t i c a l S e c t i o n函數將會產生一個E X C E P T I O N _ I N VA L I D _ H A N D L E異常。大多數編程人員忽略了這個潛在的錯誤,在他們的代碼中沒有專門的處理方法,因為這個錯誤非常少見。但是,如果想對這種情況有所準備,可以有兩種選擇。
? ? 可以使用結構化異常處理方法來跟蹤錯誤。當錯誤發生時,既可以不訪問關鍵代碼段保護的資源,也可以等待某些內存變成可用狀態,然后再次調用 E n t e r C r i t i c a l S e c t i o n函數。
另一種選擇是使用I n i t i a l i z e C r i t i c a l S e c t i o n A n d S p i n C o u n t函數創建關鍵代碼段,確保設置了d w S p i n C o u n t參數的高信息位。當該函數發現高信息位已經設置時,它就創建該事件內核對象,并在初始化時將它與關鍵代碼段關聯起來。如果事件無法創建,該函數返回 FA L S E。可以更加妥善地處理代碼中的這個事件。如果事件創建成功,你知道 E n t e r C r i t i c a l S e c t i o n將始終都能運行,并且決不會產生異常情況(如果總是預先分配事件內核對象,就會浪費系統資源。只有當你的代碼不能容許E n t e r C r i t i c a l S e c t i o n運行失敗,或者你有把握會出現爭用現象,或者你預計進程將在內存非常短缺的環境中運行時,你才能預先分配事件內核對象) 。
?
總結
以上是生活随笔為你收集整理的Windows核心编程 第八章 用户方式中线程的同步(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows核心编程 第八章 用户方式
- 下一篇: Windows核心编程 第九章 线程与内