讀Windows核心編程 - 9

    上一章我們介紹了用戶方式的線程同步,它的優點是速度非常快。但是它也有其侷限性,比如互鎖函數只能在單值上運行,根本無法使線程進入等待狀態。使用關鍵代碼段可以使線程進入等待狀態,但是隻能用這些代碼段對單個進行中的線程進行同步。另外,使用關鍵代碼段容易導致死鎖,因爲在等待進入關鍵代碼段時無法設定超時值。

    這一章中我們要介紹的是內核方式的線程同步,它唯一的確定就是速度比較慢。當調用本章提到的任何函數時,調用線程必須從用戶方式轉爲內核方式,而這個過程通常需要代價,大概需要1000個CPU週期。前面我們已經介紹了若干種內核對象,進行、線程和作業等,這些內核對象都可以用於線程同步。對於線程的同步來說,這些內核對象總是處於未通知或者已通知狀態。以進程爲例:進程剛創建是始終處於未通知狀態,當進程終止時,操作系統自動使該進程內核對象處於已通知狀態。線程內核對象也如此。下面的內核對象可以處於未通知或者已通知狀態:進程、線程、作業、文件修改通知、事件、可等待定時器、文件、控制檯輸入、信標、互斥對象。Microsoft爲每一種內核對象定義了一種規則來控制未通知/已通知狀態。

等待函數:

    等待函數可使線程自願進入等待狀態,直到一個特定的內核對象變成已通知狀態爲止。最常用的函數是WaitForSingleObject:

DWORD WaitForSingleObject(
    HANDLE hObject,
    DWORD  dwMilliseconds);

    第一個參數是能支持未通知/已通知狀態的內核對象的句柄,第二個參數表示等待的時間。我們可以傳入INFINITE作爲第二個參數表示無限等待,直到等待內核對象變爲已通知狀態。但是這樣有些危險,如果該內核對象永遠處於未通知狀態那麼調用線程永遠不能被調用,不過,它不會浪費CPU時間。我們可以爲第二個參數傳遞一個普通的值,例如5000。這個時候可以用一個switch/case語句來檢查WaitForSingleObject的返回狀態。

DWORD dw = WaitForSingleObject(hProcess, 5000);
switch(dw){
    case WAIT_OBJECT_0: // The process terminated
        break;
    case WAIT_TIMEOUT:  // The process not terminated in 5000 ms.
        break;
    case WAIT_FAILED:   // Bad call to function(invalid handle?)
        break;
}

    如果返回值是WAIT_FAILED,我們可以調用GetLastError瞭解詳細信息。下面這個函數與WaitForSingleObject類似:

DWORD WaitForMultipleObjects(
    DWORD dwCount,
    CONST HANDLE* phObjects,
    BOOL  fWaitALL,
    DWORD dwMilliseconds); 

    第一個參數指定等待的內核對象的數量。第三個參數如果是TRUE,表明只有當等待的所有內核對象都變爲已通知狀態時該線程纔可調用,如果是FALSE,只需其中任意一個內核對象變爲已通知狀態即可。如果fWaitALL是TRUE,那麼返回值WAIT_OBJECT_0表示等待的所有內核對象都變爲已通知狀態。如果fWaitALL是FALSE,我們怎麼判斷返回的哪個對象變爲已通知狀態呢?如果返回WAIT_OBJECT_0+0表示phObjects中的第一個內核對象變爲已通知狀態,返回WAIT_OBJEC_0+1表示第二個,以此類推。

成功等待的副作用:

    對於有些內核對象來說,成功調用WaitForSingleObject和WaiForMultipleObjects,實際上會改變對象的狀態。成功地調用是指函數發現對象已經得到通知並且返回一個相對於WAIT_OBJECT_0的值。當這樣的對象的狀態改變時,我們就稱之爲成功等待的副作用。不同的內核對象有不同的副作用,而有些則沒有副作用。這些副作用將在具體講到哪個內核對象時一一介紹。

    如果多個線程等待單個內核對象,那麼當該內核對象變爲已通知狀態時,系統究竟決定喚醒哪個進程呢?Microsoft對這個問題的正式回答是:算法是公平的。這意味這線程的優先級將不起任何作用,還意味着等待時間最長的線程不一定得到該對象。然而在實際操作中,Microsoft使用的算法是常用的“先進先出”的方案。等待了最長時間的線程得到該對象。但是系統中將會執行一些操作,以便改變這個行爲特性,使它不太容易預測,這也是Microsoft沒有明確說明算法如何起作用的原因。

事件內核對象:

    事件內核對象是最基本的對象,它包含一個使用計數(與所有內核對象相同),一個用於指明該事件是個自動重置的事件還是一個人工重置的布爾值,另一個用於指明該事件處於未通知還是已通知狀態的布爾值。當人工重置的事件得到通知時,等待該事件的所有線程均變爲可調度線程,當自動重置的事件得到通知時,等待該事件的線程中只有一個線程變爲可調度。

    當一個線程執行初始化操作,然後通知另一個線程執行剩餘的操作時,事件對象使用最多。創建事件內核對象使用如下:

CreateEvent(
    PSECURITY_ATTRIBUTES pas,
    BOOL fManualReset,       // 人工重置or自動重置
    BOOL fInitialState,      // 初始化狀態,未通知or已通知
    PCTSTR pszName);

    一旦事件已經創建,我們就可以調用SetEvent(HANDLE ...)將事件變爲已通知狀態,也可以調用ResetEvent(HANDLE ...)將事件設爲未通知狀態。Microsoft爲自動重置的事件定義了成功等待的副作用規則,即當線程成功等待到該對象時,自動重置的事件就會自動重置爲未通知狀態,通常沒有必要爲自動重置的事件調用ResetEvent函數,因爲系統會自動對事件進行重置。但是,Microsoft沒有爲人工重置的事件定義成功等待的副作用。

    另外還有一個函數:BOOL PulseEvent(HANDLE hEvent),該函數使得事件變爲已通知狀態,然後又立即變爲未通知狀態,就像調用了SetEvent之後立即調用了ResetEvent一樣。由於在調用PulseEvent時無法知道任何線程的狀態,因此該函數並不那麼有用。

等待定時器內核對象:

    等待定時器是在某個時間或按規定的間隔發出自己的信號通知的內核對象。要創建等待定義器只需調用如下函數:

HANDLE CreateWaitableTimer{
    PSECURITY_ATTRIBUTES psa,
    BOOL fManualReset,
    PCTSRT pszName);

    於事件的情況一樣,fManualReset參數用於指明人工重置定時器或自動重置定時器。當發出人工重置的定時器信號通知時,等待該定時器的所有線程均變爲可調度線程。當發出自動重置的定時器信號通知時,只有一個等待的線程變爲可調度線程。

    等待定時器對象總是在未通知狀態中創建。必須調用SetWaitableTimer函數來告訴定時器你想在何時讓它變爲已通知狀態:

BOOL SetWaitableTimer(
    HANDLE hTimer,
    const LARGE_INTEGER *pDueTime,
    LONG lPeriod,
    PTIMRAPCROUTINE pfnCompletionRoutine,
    PVOID pvArgTomCompletionRoutine,
    BOOL fResume);

    pDueTime和lPeriod兩個參數分別表示定時器何時應該第一次報時及報時間隔。具體使用方法參看核心編程page206. fResume參數用於支持暫停和恢復的計算機,參看核心編程page207. 每次調用SetWaitableTimer時都會設置新的報時條件並撤銷定時器原來的條件。我們還可以使用CancelWaitableTimer函數來撤銷定時器。

    現在我們來比較一下用戶定時器(用SetTimer進行設置)和等待定時器。用戶定時器能夠生產WM_TIMER消息,這些消息將返回給調用SetTimer的線程和創建窗口的線程。因此,當用戶定時器報時的時候只有一個線程得到通知。另一方面,等待定時器可以供多個線程共享。如果要執行與用戶界面相關的事件,以便對定時器作出響應,那麼使用用戶定時器來組織代碼結構可能更加容易些,因爲使用等待定時器時,線程必須既要等待各種消息又要等待內核對象(如果要改變代碼結構,可以使用MsgWaitForMultileObjects函數)。最後,運用等待定時器,當到了規定時間的時候,更有可能得到通知。因爲WM_TIMER消息始終屬於最低優先級的消息,當線程的隊列中沒有其他消息時才檢索該消息。

信標內核對象:

    創建信標內核對象的方法如下:

HANDLE CreateSemaphore( PSECURITY_ATTRIBUTE psa, LONG lInitialCount, LONG lMaximumCount, PCTSTR pszName

);

    lMaximumCount參數告訴系統,應用程序最大資源數量是多少,lInitialCount告訴系統開始時這些資源中有多少可供使用。通過調用等待函數,傳遞負責保護資源的信標的句柄,線程就能獲得對該資源的訪問權。從內部來說,該等待函數要檢查信標的當前資源數量,如果它的值大於0(信標已經發出信號),那麼計數器減1,調用線程保持可調度狀態。這個過程中其他線程不得干擾。通過調用ReleaseSemaphore函數,線程就能夠對信標的當前資源數量進行遞增。其中有個lReleaseCount表示用於遞增的數量。

互斥對象內核對象:

    互斥對象擁有一個使用數量(同其他內核對象),一個線程ID用於記錄當前哪個線程擁有內核對象,一個遞歸計數器用於指定該線程擁有互斥對象的次數。

    互斥對象的使用規則如下:

    1. 如果線程ID是0,互斥對象不被任何線程所擁有,並且發出該互斥對象的通知信號。

    2. 如果ID個是非0數字,那麼一個線程就擁有這個互斥對象,不發出通知信號。

    3. 與所有其他內核對象不同,互斥對象在操作系統中擁有特殊的代碼,允許它們違反正常的規則。

    互斥對象的創建方法如下:

HANDLE CreateMutex(
    DWORD fdwAccess,
    BOOL fInitialOwner,
    PCTSTR pszName);

    如果fInitialOwner是FALSE,那麼互斥對象的ID和計數器都是0,因此發出通知信號。如果是TRUE,那麼線程ID被設置爲調用線程的ID,遞歸計數器被設置爲1,不發出通知信號。對於互斥對象來說,正常的內核的已通知和未通知規則存在一個特殊的異常情況。比如說,一個線程試圖等待一個未通知的互斥對象,通常情況下,這個線程會被置於等待狀態。然而,系統要查看試圖獲取互斥對象的線程的ID是否與互斥對象中記錄的ID相同,如果兩個ID相同,即使互斥對象處於未通知狀態,系統也允許該線程進入可調度狀態,同時遞歸計數器遞增,這也是遞歸計數器>1的唯一情況。

    當線程不再需要訪問權時,必須調用ReleaseMutex函數來釋放該互斥對象。該函數使遞歸計數器減1,值得注意的是,如果一個線程多次成功的等待一個互斥對象,那麼必須調用相同次數的ReleaseMutex函數來釋放,只有當遞歸計數器變爲0時,該線程ID也設置爲0,同時對象變爲已通知狀態。

    現在我們來看一下互斥內核對象的釋放問題,當我們調用ReleaseMutex時,系統將檢查調用線程的ID和互斥對象記錄的線程ID是否相同,如果不同ReleaseMutex不進行任何操作並返回FALSE。這裏會遇到一個問題,如果一個線程擁有該互斥對象後意外終止會發生什麼情況?這時,系統自動將遞歸計數器和線程ID設置爲0,如果有線程正在等待這個互斥對象,那麼系統會“公平地”選取一個線程變爲可調度狀態。注意:這個時候WaitForSingleObject這樣的函數會返回WAIT_ABANDONED值,這表示線程正在等待的互斥對象是由另外一個線程擁有的,而這個線程已經在它完成對共享資源的使用前終止了,這通常不是一個最近進入狀態,因爲新調度的線程不知道目前資源處於何種狀態。

    下面我們來比較一下互斥對象和關鍵代碼段的區別,互斥對象運行速度較慢,但是可以跨進程使用,而且可以設定任意的等待時間,而關鍵代碼段則正好相反。

線程同步對象速查表:

對象 何時處於未通知狀態 何時處於統治狀態 成功等待的副作用
進程 當進程仍然活動時 當進程終止時
線程 當線程仍然活動時 當現程終止時
作業 當作業的時間尚未結束時 當作業的時間已經結束時
文件 當I/O請求正在處理時 當I/O請求處理完畢時
控制檯輸入 不存在任何輸入時 當存在輸入時
文件修改通知 沒有任何文件被修改 當文件系統發現被修改時 重置通知
自動重置事件 ResetEvent、PulseEvent或等待成功 調用SetEvent、PulseEvent時 重置事件
人工重置事件 ResetEvent或PulseEvent 調用SetEvent、PulseEvent時
自動重置的定時器 CancelWaitableTimer或等待成功 當時間到 重置定時器
人工重置的定時器 CancelWaitableTimer 當時間到
信標 等待成功

當數量>0時

ReleaseSemaphore

數量遞減1
互斥對象 等待成功

當未被線程擁有時

ReleaseMutex

將所有權賦予線程

關鍵代碼段

(用戶方式)

等待成功

當未被線程擁有時

LeaveCriticalSection

將所有權賦予線程
... ... ... ...
... ... ... ...

其他線程同步函數:

    WaitForInputIdel: 這個函數一直處於等待狀態,直到hProcess標識的進程在創建應用程序的第一個窗口的線程中已經沒有尚未處理的輸入爲止。

    MsgWaitForMultipleObjects: 這個函數與WaitForMultipleObjects相似,差別在於它們允許線程在內核對象變爲已通知狀態或窗口消息需要調度到調用線程創建的窗口中時被調度。

    WaitForDebugEvent: 當調試程序調用該函數時,調試程序終止運行,系統將調試事件已經發生的情況通知調試程序,方法是允許調用的WaitForDebugEvent返回,並由系統填入相關參數。

    SingleObjectAndWait: 這個函數以原子的操作發出關於內核對象的通知並等待另一個內核對象。這個函數有兩個功能:第一減少了系統的運行時間,如果按照普通的方式調用需要2次內核方式的轉換,而這個函數只有一次。第二,如果沒有這個函數,那麼我們需要先Release再WaitForSingleObject完成相應功能,但是在調用完Release後可能會立即切換到其他線程運行(前提是該線程正在等待Release的內核對象),這個時候Release和WaitForSingleObject就不能緊接着執行,可能會有非預期的結果產生,具體參看核心編程page226.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章