多線程面試題系列(5):經典線程同步 關鍵段CS
上一篇提出了一個經典的多線程同步互斥問題,本篇將用關鍵段CRITICAL_SECTION來嘗試解決這個問題。本文首先介紹下如何使用關鍵段,然後再深層次的分析下關鍵段的實現機制與原理。關鍵段CRITICAL_SECTION一共就四個函數,使用很是方便。下面是這四個函數的原型和使用說明。
函數功能:初始化
函數原型:
void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函數說明:定義關鍵段變量後必須先初始化。
函數功能:銷毀
函數原型:
void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函數說明:用完之後記得銷毀。
函數功能:進入關鍵區域
函數原型:
void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函數說明:系統保證各線程互斥的進入關鍵區域。
函數功能:離開關關鍵區域
函數原型:
void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
然後在經典多線程問題中設置二個關鍵區域。一個是主線程在遞增子線程序號時,另一個是各子線程互斥的訪問輸出全局資源時。詳見代碼:
[cpp] view plain copy- #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.推薦關鍵段與旋轉鎖配合使用。
下一篇將介紹使用事件Event來解決這個經典線程同步問題。
多線程面試題系列(5):經典線程同步 關鍵段CS