上一篇討論有關“互鎖函數家族”處理線程同步的方法。本書中還描述了“快取行”(cache line)概念,還給了一些線程同步的建議,比如要實現單個變數的同步,應該避免使用volatile關鍵字,如果實在需要對單個變數進行同步,最好使用互鎖函數(傳遞的是地址,所以每次取值都從記憶體取得)。感覺這些東西沒有什麼好講的,看看書就可以了。
然後,本書提供了另外一種工作在使用者模式的線程同步的方法:關鍵程式碼片段。雖然不能協調多個進程中的線程,但是我確實最經常使用這種機制來協調單個進程中線程的同步(因為好用^_^)。
關鍵程式碼片段,所謂“程式碼片段”,也就是說“一段代碼”。這段代碼的執行是以原子的形式執行的,獨佔著某些資源,任何想要訪問這些資源的其他線程在關鍵程式碼片段執行完成之前只能等待。
要讓實現關鍵程式碼片段,需要以下5個步驟:
1、初始化一個關鍵程式碼片段結構。
2、一個線程進入關鍵程式碼片段。
3、對資源進行原子操作。
4、該線程離開關鍵程式碼片段。
5、刪除關鍵程式碼片段。
上述5個步驟,除了第3步是程式員需要自己進行設計,其他都有相應的API函數可以被調用。
首先看下關鍵程式碼片段結構:CRITICAL_SECTION。這個資料結構是有明確文檔定義的,但是微軟認為裡面的內容不需要我們去瞭解。所以該結構內部的成員對我們來說是透明的。該結構在WinBase.h檔案中被定義為RTL_CRITICAL_SECTION。
在第1步,你需要初始化這個結構,調用InitializeCriticalSection函數,傳遞一個該結構的指標。在該函數內部會設定CRITICAL_SECTION的內部成員。
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
在第5步,當你不再需要這個關鍵程式碼片段的時候,你需要呼叫函數DeleteCriticalSection來清除CIRTICLA_SECTION結構,同樣是傳遞一個該結構的指標。
VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);
當你調用InitializeCriticalSection函數初始化了一個關鍵程式碼片段之後,你就可以讓你的線程通過這個關鍵程式碼片段來對某寫資源進行原子訪問。
在第2步,你需要進入一個關鍵程式碼片段,呼叫函數EnterCriticalSection,同樣傳遞一個已經初始化了的CRITICAL_SECTION結構指標。
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
當調用該函數的時候,會發生以下三種的處理:
如果沒有其他線程在訪問相關資源,那麼該函數更新內部資料,指明當前線程已經被賦予訪問權並立即返回,使得該線程可以繼續執行。
如果CRITICAL_SECTION結構的成員變數指明了當前線程已經被賦予了訪問權,則更新內部資料,指明該線程被賦予了多少次訪問權(遞增計數)。這種情況比較少見,只有在一個線程內部多次調用EnterCriticalSection函數才會發生。
如果CRITICAL_SECTION結構的成員變數指明了當前已經有一個線程被賦予了訪問權,那麼該函數將當前線程設定為等待狀態。然後更新內部資料,一旦正在訪問資源的線程離開的關鍵程式碼片段(調用LeaveCritlcalSection,後面會講),該線程就會處於可調度狀態。
如果當前已經初始化了一個關鍵程式碼片段cs,同時存在著2個線程:T1和T2。然後T1呼叫EnterCriticalSection(&cs)函數進入該關鍵程式碼片段,然後T2也呼叫EnterCriticalSection(&cs),那麼T2會進入等待狀態,等待T1離開cs所代表的關鍵程式碼片段後,T2才恢複到可調度狀態。
實際上,等待的線程可能會逾時,然後拋出一個異常。該時間數值由註冊表中的一個值表示的:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager
該值預設為2592000s,即大約3 0天。
從內部來講,EnterCriticalSection函數並不複雜,在內部使用互鎖函數,執行的只是一些簡單的測試。但是這些測試都是以原子的方式進行的。
你可以使用函數TryEnterCriticalSection來代替EnterCriticalSection。
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);
該函數不會讓呼叫的線程進入等待狀態,相反,該函數返回一個BOOL變數,指明當前線程能否進入關鍵程式碼片段。
該函數可以讓線程快速地查看能否擷取某些資源,如果不能,該線程可以繼續做其他的事情。如果該函數返回TRUE,說明CRITICAL_SECTION的成員變數已經更新,可以進入對應的關鍵程式碼片段了。
如果一個線程順利地進入了關鍵程式碼片段,那麼意味著它可以獨佔某些資源,此時可以以特定的演算法來對某些資源訪問和操作了(對應步驟3)。
第4步,在一個線程對某些資源訪問或操作完成之後,必須離開關鍵程式碼片段,調用函數LeaveCriticalSection函數,同樣傳遞一個CRITICAL_SECTION結構的指標。
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
調用該函數的時候,CRITICAL_SECTION的內部資料會被更新。該函數使計數遞減1,指明當前線程被賦予的訪問權次數。
如果遞減後,該計數仍然大於0,則該函數什麼也不做,只是簡單的返回而已。
遞減後,如果該計數等於0,它就更新成員變數,並查看那些因為調用EnterCriticalSection而處於等待狀態的線程,如果存在這樣的線程,它就更新成員變數,並選擇其中一個,讓其變成可調度狀態;如果沒有線程在等待,則更新成員變數,說明此時沒有線程在訪問資源。
也就是說,一個EnterCriticalSection函數必須有一個LeaveCriticalSection函數與之對應,否則一個線程會一直獨佔了某些資源,即使該線程結束之後,這些資源也被關鍵程式碼片段鎖定。而其他線程如果調用EnterCriticalSection進入該關鍵程式碼片段是無法訪問這些資源的。
下面討論有關“關鍵程式碼片段與迴圈鎖”的問題。
如果一個關鍵程式碼片段已經被其他線程所擁有,那麼如果當前線程試圖進入這個關鍵程式碼片段的時候,會立即被設定為等待狀態。意味著該線程必須從使用者模式轉入核心模式,大約需要1000個CPU周期。這種轉換是需要付出代價的。實際上,在多CPU電腦上,當前擁有資源的線程可能正執行在另一個CPU上,這樣,它很可能會馬上離開關鍵程式碼片段,釋放相關資源。
為了提高關鍵程式碼片段的效能,微軟為關鍵程式碼片段提供了迴圈鎖機制。當一個線程調用EnterCriticalSection函數的時候,可以使用迴圈鎖進行迴圈查詢,這樣就可以多次嘗試訪問資源。只有當每次嘗試均告失敗之後,該線程才轉入核心模式。
如此一來,只要在這組嘗試失敗以前,原先佔有資源的線程離開了關鍵程式碼片段,那麼該嘗試訪問資源的線程便可嘗試成功,這樣避免了轉入核心模式的執行,提高的效能。
要將迴圈鎖用於關鍵程式碼片段,必須將一個關鍵程式碼片段與一個迴圈次數關聯起來,可以調用InitializeCriticalSectionAndSpinCount函數,即能夠初始化關鍵程式碼片段,也可以將一個迴圈鎖查詢次數與之綁定。
BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs, //關鍵程式碼片段結構指標
DWORD dwSpinCount); //迴圈鎖迴圈查詢次數(嘗試訪問資源次數)
要注意的情況是,如果在單CPU的電腦上,該函數的第二個參數dwSpinCount會被忽略,永遠為0,因為在單CPU上,如果一個線程在迴圈嘗試請求資源,而當前擁有資源的線程不可能被調度,資源是無法釋放的。
也可以修改一個關鍵程式碼片段迴圈鎖迴圈次數:
DWORD SetCriticalSectionSpinCount(
PCRITICAL_SECTION pcs, //關鍵程式碼片段結構指標
DWORD dwSpinCount); //迴圈鎖迴圈次數
當然,如果運行在單CPU電腦上,dwSpinCount參數會被忽略。
一般地,經驗告訴我們,設定dwSpinCount為4000,即讓線程迴圈4000次來嘗試擷取資源。
InitializeCriticalSection函數可能會運行失敗(在資源極度貧乏的情況下),由於微軟忽略了這個問題,所以它的傳回型別是VOID。在這種情況下,你可以使用InitializeCriticalSectionAndSpinCount函數,它返回一個BOOL型資料,指明初始化關鍵程式碼片段是否成功。
當使用關鍵程式碼片段的時候,可能會出現對關鍵程式碼片段的爭用,即當前線程調用EnterCriticalSection函數的時候,該關鍵程式碼片段的訪問權已經被另一個線程所擁有,此時發生了爭用。此時關鍵程式碼片段使用事件核心對象處理線程同步問題。
當在記憶體資源極度貧乏的情況下,此時線程爭用關鍵程式碼片段,那麼關鍵程式碼片段可能無法建立必要的事件核心對象,這個時候EnterCriticalSection函數會產生一個EXCEPTION_INVALID_HANDLE異常,你可以採取一下兩種方法處理之:
1、使用結構化異常的方法,當異常產生的時候,不訪問關鍵程式碼片段保護的資源,當記憶體變成可用狀態的時候,再次呼叫EnterCriticalSection函數。
2、使用InitializeCriticalSectionAndSpinCount函數建立關鍵程式碼片段的時候確保設定了dwSpinCount參數的高資訊位,當該函數發現dwSpinCount的高資訊位被設定,它會建立一個事件核心對象,並將該核心對象與關鍵程式碼片段關聯起來。如果事件核心對象無法建立,函數返回FALSE。如果建立成功那麼就意味著EnterCriticalSection總能運行成功,因為總是先建立事件核心對象。