[一个经典的多线程同步问题]解决方案一:关键段CS
前面提出了一個經典的多線程同步互斥問題,本篇將用關鍵段CRITICAL_SECTION來嘗試解決這個問題。
本文先介紹如何使用關鍵段,然后再深層次的分析下關鍵段的實現機制和原理。
關鍵段CRITICAL_SECTION一共就四個函數,下面說一下這四個函數的原型和使用。
函數功能:初始化 函數原型: void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection); 函數說明:定義關鍵段變量后必須先初始化。函數功能:銷毀 函數原型: void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection); 函數說明:用完之后記得銷毀。函數功能:進入關鍵區域 函數原型: void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection); 函數說明:系統保證各線程互斥的進入關鍵區域。函數功能:離開關關鍵區域 函數原型: void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);然后在前面的這個經典多線程問題中設置兩個關鍵區域。一個是主線程在遞增子線程序號時,另一個是各個線程互斥的訪問輸出全局資源。
#include <stdio.h> #include <process.h> #include <windows.h>long g_nCount; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //關鍵段變量聲明 CRITICAL_SECTION g_csThreadParameter, g_csThreadCode;int main() {//關鍵段初始化InitializeCriticalSection(&g_csThreadParameter);InitializeCriticalSection(&g_csThreadCode);HANDLE handle[THREAD_NUM];g_nCount = 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);EnterCriticalSection(&g_csThreadCode);//進入各子線程的互斥區g_nCount++;Sleep(0);printf("線程編號為%d 全局資源值為%d\n", nThreadNum, g_nCount);LeaveCriticalSection(&g_csThreadCode);//離開各個子線程的互斥區域return 0; }運行結果:
可以看到各個子線程能夠成功的訪問和輸出全局資源了,但是主線程和子線程之間的同步還是有點問題。
想找到問題,最直接的方法就是在程序中添加斷點來查看程序的執行流程:
正常來說,兩個斷點應該輪流執行才可以,但是實際的調試過程中卻發現實際的情況不是如此。在沒有運行第二個斷點的情況下,主線程可以多次通過第一個斷點,也就是:
?EnterCriticalSection(&g_csThreadParameter);?
這個語句。這說明主線程可以多次進入這個關鍵區,而不用子線程去幫助它釋放關鍵區的變量。
找到了主線程不能和子線程同步的原因了,那么為什么他們功能用關鍵段進行同步呢?
先找到關鍵段CRITICAL_SECTION的定義吧,它在WinBase.h中被定義成RTL_CRITICAL_SECTION。而RTL_CRITICAL_SECTION在WinNT.h中聲明,它其實是個結構體:
typedef struct _RTL_CRITICAL_SECTION {PRTL_CRITICAL_SECTION_DEBUG DebugInfo;LONG LockCount;LONG RecursionCount;HANDLE OwningThread; // from the thread's ClientId->UniqueThreadHANDLE LockSemaphore;DWORD SpinCount; } RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION; 各個參數的解釋如下: 第一個參數:PRTL_CRITICAL_SECTION_DEBUG DebugInfo; 調試用的。 第二個參數:LONG LockCount; 初始化為-1,n表示有n個線程在等待。 第三個參數:LONG RecursionCount; 表示該關鍵段的擁有線程對此資源獲得關鍵段次數,初為0。 第四個參數:HANDLE OwningThread; 即擁有該關鍵段的線程句柄,微軟對其注釋為——from the thread's ClientId->UniqueThread 第五個參數:HANDLE LockSemaphore; 實際上是一個自復位事件。 第六個參數:DWORDSpinCount; 旋轉鎖的設置,單CPU下忽略由這個結構可以知道關鍵段會記錄擁有該關鍵段的線程句柄即關鍵段是有“線程所有權”概念的。事實上它會用第四個參數OwningThread來記錄獲準進入關鍵區域的線程句柄,如果這個線程再次進入,EnterCriticalSection()會更新第三個參數RecursionCount以記錄該線程進入的次數并立即返回讓該線程進入。其它線程調用EnterCriticalSection()則會被切換到等待狀態,一旦擁有線程所有權的線程調用LeaveCriticalSection()使其進入的次數為0時(注意這句話的意思是Enter和Leave必須在同一個線程中),系統會自動更新關鍵段并將等待中的線程換回可調度狀態。
因此可以將關鍵段比作旅館的房卡,調用EnterCriticalSection()即申請房卡,得到房卡后自己當然是可以多次進出房間的,在你調用LeaveCriticalSection()交出房卡之前,別人自然是無法進入該房間。
回到這個經典線程同步問題上,主線程正是由于擁有“線程所有權”即房卡,所以它可以重復進入關鍵代碼區域從而導致子線程在接收參數之前主線程就已經修改了這個參數。所以關鍵段可以用于線程間的互斥,但不可以用于同步。
另外,由于將線程切換到等待狀態的開銷較大,因此為了提高關鍵段的性能,Microsoft將旋轉鎖合并到關鍵段中,這樣EnterCriticalSection()會先用一個旋轉鎖不斷循環,嘗試一段時間才會將線程切換到等待狀態。下面是配合了旋轉鎖的關鍵段初始化函數。
函數功能:初始化關鍵段并設置旋轉次數 函數原型: BOOLInitializeCriticalSectionAndSpinCount(LPCRITICAL_SECTIONlpCriticalSection,DWORDdwSpinCount); 函數說明:旋轉次數一般設置為4000。函數功能:修改關鍵段的旋轉次數 函數原型: DWORDSetCriticalSectionSpinCount(LPCRITICAL_SECTIONlpCriticalSection,DWORDdwSpinCount);最后總結下關鍵段:
1.關鍵段共初始化化、銷毀、進入和離開關鍵區域四個函數。
2.關鍵段可以解決線程的互斥問題,但因為具有“線程所有權”,所以無法解決同步問題。
3.推薦關鍵段與旋轉鎖配合使用。
轉載于:https://www.cnblogs.com/stemon/p/4390189.html
總結
以上是生活随笔為你收集整理的[一个经典的多线程同步问题]解决方案一:关键段CS的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信小程序入门四: 导航栏样式、tabB
- 下一篇: 三维空间中无人机路径规划的改进型蝙蝠算法