秒殺多執行緒第五篇 經典執行緒同步 關鍵段CS
上一篇《秒殺多執行緒第四篇 一個經典的多執行緒同步問題》提出了一個經典的多執行緒同步互斥問題,本篇將用關鍵段CRITICAL_SECTION來嘗試解決這個問題。
本文首先介紹下如何使用關鍵段,然後再深層次的分析下關鍵段的實現機制與原理。
關鍵段CRITICAL_SECTION一共就四個函式,使用很是方便。下面是這四個函式的原型和使用說明。
函式功能:初始化
函式原型:
void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函式說明:定義關鍵段變數後必須先初始化。
函式功能:銷燬
函式原型:
void DeleteCriticalSection
函式說明:用完之後記得銷燬。
函式功能:進入關鍵區域
函式原型:
void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函式說明:系統保證各執行緒互斥的進入關鍵區域。
函式功能:離開關關鍵區域
函式原型:
void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
然後在經典多執行緒問題中設定二個關鍵區域。一個是主執行緒在遞增子執行緒序號時,另一個是各子執行緒互斥的訪問輸出全域性資源時。詳見程式碼:
#include <stdio.h>#include <process.h>#include <windows.h>long g_nNum;unsigned int __stdcall Fun(void *pPM);const int THREAD_NUM = 10;//關鍵段變數宣告CRITICAL_SECTION g_csThreadParameter, g_csThreadCode;int main(){ printf(" 經典執行緒同步 關鍵段\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n" ); //關鍵段初始化 InitializeCriticalSection(&g_csThreadParameter); InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while (i < THREAD_NUM) { EnterCriticalSection(&g_csThreadParameter);//進入子執行緒序號關鍵區域 handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); ++i; } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); DeleteCriticalSection(&g_csThreadCode); DeleteCriticalSection(&g_csThreadParameter); return 0;}unsigned int __stdcall Fun(void *pPM){ int nThreadNum = *(int *)pPM; LeaveCriticalSection(&g_csThreadParameter);//離開子執行緒序號關鍵區域 Sleep(50);//some work should to do EnterCriticalSection(&g_csThreadCode);//進入各子執行緒互斥區域 g_nNum++; Sleep(0);//some work should to do printf("執行緒編號為%d 全域性資源值為%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode);//離開各子執行緒互斥區域 return 0;}
執行結果如下圖:
可以看出來,各子執行緒已經可以互斥的訪問與輸出全域性資源了,但主執行緒與子執行緒之間的同步還是有點問題。
這是為什麼了?
要解開這個迷,最直接的方法就是先在程式中加上斷點來檢視程式的執行流程。斷點處置示意如下:
然後按F5進行除錯,正常來說這兩個斷點應該是依次輪流執行,但實際除錯時卻發現不是如此,主執行緒可以多次通過第一個斷點即
EnterCriticalSection(&g_csThreadParameter);//進入子執行緒序號關鍵區域
這一語句。這說明主執行緒能多次進入這個關鍵區域!找到主執行緒和子執行緒沒能同步的原因後,下面就來分析下原因的原因吧^_^
先找到關鍵段CRITICAL_SECTION的定義吧,它在WinBase.h中被定義成RTL_CRITICAL_SECTION。而RTL_CRITICAL_SECTION在WinNT.h中宣告,它其實是個結構體:
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUGDebugInfo;
LONGLockCount;
LONGRecursionCount;
HANDLEOwningThread; // from the thread's ClientId->UniqueThread
HANDLELockSemaphore;
DWORDSpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
各個引數的解釋如下:
第一個引數:PRTL_CRITICAL_SECTION_DEBUGDebugInfo;
除錯用的。
第二個引數:LONGLockCount;
初始化為-1,n表示有n個執行緒在等待。
第三個引數:LONGRecursionCount;
表示該關鍵段的擁有執行緒對此資源獲得關鍵段次數,初為0。
第四個引數:HANDLEOwningThread;
即擁有該關鍵段的執行緒控制代碼,微軟對其註釋為——from the thread's ClientId->UniqueThread
第五個引數:HANDLELockSemaphore;
實際上是一個自復位事件。
第六個引數:DWORDSpinCount;
旋轉鎖的設定,單CPU下忽略
由這個結構可以知道關鍵段會記錄擁有該關鍵段的執行緒控制代碼即關鍵段是有“執行緒所有權”概念的。事實上它會用第四個引數OwningThread來記錄獲准進入關鍵區域的執行緒控制代碼,如果這個執行緒再次進入,EnterCriticalSection()會更新第三個引數RecursionCount以記錄該執行緒進入的次數並立即返回讓該執行緒進入。其它執行緒呼叫EnterCriticalSection()則會被切換到等待狀態,一旦擁有執行緒所有權的執行緒呼叫LeaveCriticalSection()使其進入的次數為0時,系統會自動更新關鍵段並將等待中的執行緒換回可排程狀態。
因此可以將關鍵段比作旅館的房卡,呼叫EnterCriticalSection()即申請房卡,得到房卡後自己當然是可以多次進出房間的,在你呼叫LeaveCriticalSection()交出房卡之前,別人自然是無法進入該房間。
回到這個經典執行緒同步問題上,主執行緒正是由於擁有“執行緒所有權”即房卡,所以它可以重複進入關鍵程式碼區域從而導致子執行緒在接收引數之前主執行緒就已經修改了這個引數。所以關鍵段可以用於執行緒間的互斥,但不可以用於同步。
另外,由於將執行緒切換到等待狀態的開銷較大,因此為了提高關鍵段的效能,Microsoft將旋轉鎖合併到關鍵段中,這樣EnterCriticalSection()會先用一個旋轉鎖不斷迴圈,嘗試一段時間才會將執行緒切換到等待狀態。下面是配合了旋轉鎖的關鍵段初始化函式
函式功能:初始化關鍵段並設定旋轉次數
函式原型:
BOOLInitializeCriticalSectionAndSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
函式說明:旋轉次數一般設定為4000。
函式功能:修改關鍵段的旋轉次數
函式原型:
DWORDSetCriticalSectionSpinCount(
LPCRITICAL_SECTIONlpCriticalSection,
DWORDdwSpinCount);
《Windows核心程式設計》第五版的第八章推薦在使用關鍵段的時候同時使用旋轉鎖,這樣有助於提高效能。值得注意的是如果主機只有一個處理器,那麼設定旋轉鎖是無效的。無法進入關鍵區域的執行緒總會被系統將其切換到等待狀態。
最後總結下關鍵段:
1.關鍵段共初始化化、銷燬、進入和離開關鍵區域四個函式。
2.關鍵段可以解決執行緒的互斥問題,但因為具有“執行緒所有權”,所以無法解決同步問題。
3.推薦關鍵段與旋轉鎖配合使用。
如果覺得本文對您有幫助,請點選‘頂’支援一下,您的支援是我寫作最大的動力,謝謝。