執行緒同步互斥的4種方式
執行緒的一些基本概念
一、執行緒的基本概念。
基本概念:執行緒,即輕量級程序(LWP:LightWeight Process),是程式執行流的最小單元。一個標準的執行緒由執行緒ID、當前指令指標(PC),暫存器集合和堆疊組成。執行緒是程序中的一個實體,是被系統獨立排程和分派的基本單位。執行緒不擁有系統資源,近擁有少量執行必須的資源。
二、執行緒的基本狀態。
基本狀態:就緒、阻塞和執行三種基本狀態。
就緒狀態,指執行緒具備執行的所有條件,邏輯上可以執行,在等待處理機;
執行狀態,指執行緒佔有處理機正在執行;
阻塞狀態,指執行緒在等待一個事件(如訊號量),邏輯上不可執行。
三、程序和執行緒的關係。
簡而言之,一個程式至少有一個程序
程序和執行緒的主要差別在於它們是不同的作業系統資源管理方式。程序有獨立的地址空間,一個程序崩潰後,在保護模式下不會對其它程序產生影響,而執行緒只是一個程序中的不同執行路徑。執行緒有自己的堆疊和區域性變數,但執行緒之間沒有單獨的地址空間,一個執行緒死掉就等於整個程序死掉,所以多程序的程式要比多執行緒的程式健壯,但在程序切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行並且又要共享某些變數的併發操作,只能用執行緒,不能用程序。
四、執行緒同步互斥的4種方式
1. 臨界區(Critical Section):適合一個程序內的多執行緒訪問公共區域或程式碼段時使用
2.
3. 事件(Event):通過執行緒間觸發事件實現同步互斥
4. 訊號量(Semaphore):與臨界區和互斥量不同,可以實現多個執行緒同時訪問公共區域資料,原理與作業系統中PV操作類似,先設定一個訪問公共區域的執行緒最大連線數,每有一個執行緒訪問共享區資源數就減一,直到資源數小於等於零。
互斥:關鍵段CS與互斥量Mutex
建立或初始化 | 銷燬 | 進入互斥區域 | 離開互斥區域 | |
關鍵段CS | Initialize- CriticalSection | Delete- CriticalSection | Enter- CriticalSection | Leave- CriticalSection |
互斥量Mutex | CreateMutex | CloseHandle | 等待系列函式如WaitForSingleObject | ReleaseMutex |
同步:
事件Event
建立 | 銷燬 | 使事件觸發 | 使事件未觸發 | |
事件Event | CreateEvent | CloseHandle | SetEvent | ResetEvent |
多條執行緒之間的互斥:
訊號量Semaphore
建立 | 銷燬 | 遞減計數 | 遞增計數 | |
訊號量 Semaphore | Create- Semaphore | CloseHandle | 等待系列函式如WaitForSingleObject | Release- Semaphore |
實現同步、互斥的四個方法的詳解:
1、《關鍵段CriticalSection》的例子
本文首先介紹下如何使用關鍵段,然後再深層次的分析下關鍵段的實現機制與原理。
關鍵段CRITICAL_SECTION一共就四個函式,使用很是方便。下面是這四個函式的原型和使用說明。
函式功能:初始化
函式原型:
void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函式說明:定義關鍵段變數後必須先初始化。
函式功能:銷燬
函式原型:
void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函式說明:用完之後記得銷燬。
函式功能:進入關鍵區域
函式原型:
void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函式說明:系統保證各執行緒互斥的進入關鍵區域。
函式功能:離開關關鍵區域
函式原型:
void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
然後在經典多執行緒問題中設定二個關鍵區域。一個是主執行緒在遞增子執行緒序號時,另一個是各子執行緒互斥的訪問輸出全域性資源時。詳見程式碼:
1. #include <stdio.h>
2. #include <process.h>
3. #include <windows.h>
4. long g_nNum;
5. unsigned int __stdcall Fun(void *pPM);
6. const int THREAD_NUM = 10;
7. //關鍵段變數宣告
8. CRITICAL_SECTION g_csThreadParameter, g_csThreadCode;
9. int main()
10.{
11. printf(" 經典執行緒同步關鍵段\n");
12. printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
13.
14. //關鍵段初始化
15. InitializeCriticalSection(&g_csThreadParameter);
16. InitializeCriticalSection(&g_csThreadCode);
17.
18. HANDLE handle[THREAD_NUM];
19. g_nNum = 0;
20. int i = 0;
21. while (i < THREAD_NUM)
22. {
23. EnterCriticalSection(&g_csThreadParameter);//進入子執行緒序號關鍵區域
24. handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
25. ++i;
26. }
27. WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
28.
29. DeleteCriticalSection(&g_csThreadCode);
30. DeleteCriticalSection(&g_csThreadParameter);
31. return 0;
32.}
33.unsigned int __stdcall Fun(void *pPM)
34.{
35. int nThreadNum = *(int *)pPM;
36. LeaveCriticalSection(&g_csThreadParameter);//離開子執行緒序號關鍵區域
37.
38. Sleep(50);//some work should to do
39.
40. EnterCriticalSection(&g_csThreadCode);//進入各子執行緒互斥區域
41. g_nNum++;
42. Sleep(0);//some work should to do
43. printf("執行緒編號為%d 全域性資源值為%d\n", nThreadNum, g_nNum);
44. LeaveCriticalSection(&g_csThreadCode);//離開各子執行緒互斥區域
45. return 0;
46.}
執行結果如下圖:
可以看出來,各子執行緒已經可以互斥的訪問與輸出全域性資源了,但主執行緒與子執行緒之間的同步還是有點問題。
這是為什麼了?
要解開這個迷,最直接的方法就是先在程式中加上斷點來檢視程式的執行流程。斷點處置示意如下:
然後按F5進行除錯,正常來說這兩個斷點應該是依次輪流執行,但實際除錯時卻發現不是如此,主執行緒可以多次通過第一個斷點即
EnterCriticalSection(&g_csThreadParameter);//進入子執行緒序號關鍵區域
這一語句。這說明主執行緒能多次進入這個關鍵區域!找到主執行緒和子執行緒沒能同步的原因後,下面就來分析下原因的原因吧^_^
先找到關鍵段CRITICAL_SECTION的定義吧,它在WinBase.h中被定義成RTL_CRITICAL_SECTION。而RTL_CRITICAL_SECTION在WinNT.h中宣告,它其實是個結構體:
typedefstruct _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.推薦關鍵段與旋轉鎖配合使用。
2、《事件Event》的例子
首先介紹下如何使用事件。事件Event實際上是個核心物件,它的使用非常方便。下面列出一些常用的函式。
第一個CreateEvent
函式功能:建立事件
函式原型:
HANDLECreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,
BOOLbManualReset,
BOOLbInitialState,
LPCTSTRlpName
);
函式說明:
第一個引數表示安全控制,一般直接傳入NULL。
第二個引數確定事件是手動置位還是自動置位,傳入TRUE表示手動置位,傳入FALSE表示自動置位。如果為自動置位,則對該事件呼叫WaitForSingleObject()後會自動呼叫ResetEvent()使事件變成未觸發狀態。打個小小比方,手動置位事件相當於教室門,教室門一旦開啟(被觸發),所以有人都可以進入直到老師去關上教室門(事件變成未觸發)。自動置位事件就相當於醫院裡拍X光的房間門,門開啟後只能進入一個人,這個人進去後會將門關上,其它人不能進入除非門重新被開啟(事件重新被觸發)。
第三個引數表示事件的初始狀態,傳入TRUR表示已觸發。
第四個引數表示事件的名稱,傳入NULL表示匿名事件。
第二個OpenEvent
函式功能:根據名稱獲得一個事件控制代碼。
函式原型:
HANDLEOpenEvent(
DWORDdwDesiredAccess,
BOOLbInheritHandle,
LPCTSTRlpName //名稱
);
函式說明:
第一個引數表示訪問許可權,對事件一般傳入EVENT_ALL_ACCESS。詳細解釋可以檢視MSDN文件。
第二個引數表示事件控制代碼繼承性,一般傳入TRUE即可。
第三個引數表示名稱,不同程序中的各執行緒可以通過名稱來確保它們訪問同一個事件。
第三個SetEvent
函式功能:觸發事件
函式原型:BOOLSetEvent(HANDLEhEvent);
函式說明:每次觸發後,必有一個或多個處於等待狀態下的執行緒變成可排程狀態。
第四個ResetEvent
函式功能:將事件設為末觸發
函式原型:BOOLResetEvent(HANDLEhEvent);
最後一個事件的清理與銷燬
由於事件是核心物件,因此使用CloseHandle()就可以完成清理與銷燬了。
在經典多執行緒問題中設定一個事件和一個關鍵段。用事件處理主執行緒與子執行緒的同步,用關鍵段來處理各子執行緒間的互斥。詳見程式碼:
1. #include <stdio.h>
2. #include <process.h>
3. #include <windows.h>
4. long g_nNum;
5. unsigned int __stdcall Fun(void *pPM);
6. const int THREAD_NUM = 10;
7. //事件與關鍵段
8. HANDLE g_hThreadEvent;
9. CRITICAL_SECTION g_csThreadCode;
10.int main()
11.{
12. printf(" 經典執行緒同步事件Event\n");
13. printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
14. //初始化事件和關鍵段自動置位,初始無觸發的匿名事件
15. g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
16. InitializeCriticalSection(&g_csThreadCode);
17.
18. HANDLE handle[THREAD_NUM];
19. g_nNum = 0;
20. int i = 0;
21. while (i < THREAD_NUM)
22. {
23. handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
24. WaitForSingleObject(g_hThreadEvent, INFINITE); //等待事件被觸發
25. i++;
26. }
27. WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
28.
29. //銷燬事件和關鍵段
30. CloseHandle(g_hThreadEvent);
31. DeleteCriticalSection(&g_csThreadCode);
32. return 0;
33.}
34.unsigned int __stdcall Fun(void *pPM)
35.{
36. int nThreadNum = *(int *)pPM;
37. SetEvent(g_hThreadEvent); //觸發事件
38.
39. Sleep(50);//some work should to do
40.
41. EnterCriticalSection(&g_csThreadCode);
42. g_nNum++;
43. Sleep(0);//some work should to do
44. printf("執行緒編號為%d 全域性資源值為%d\n", nThreadNum, g_nNum);
45. LeaveCriticalSection(&g_csThreadCode);
46. return 0;
47.}
執行結果如下圖:
可以看出來,經典線執行緒同步問題已經圓滿的解決了——執行緒編號的輸出沒有重複,說明主執行緒與子執行緒達到了同步。全域性資源的輸出是遞增的,說明各子執行緒已經互斥的訪問和輸出該全域性資源。
現在我們知道了如何使用事件,但學習就應該要深入的學習,何況微軟給事件還提供了PulseEvent()函式,所以接下來再繼續深挖下事件Event,看看它還有什麼祕密沒。
先來看看這個函式的原形:
第五個PulseEvent
函式功能:將事件觸發後立即將事件設定為未觸發,相當於觸發一個事件脈衝。
函式原型:BOOLPulseEvent(HANDLEhEvent);
函式說明:這是一個不常用的事件函式,此函式相當於SetEvent()後立即呼叫ResetEvent();此時情況可以分為兩種:
1.對於手動置位事件,所有正處於等待狀態下執行緒都變成可排程狀態。
2.對於自動置位事件,所有正處於等待狀態下執行緒只有一個變成可排程狀態。
此後事件是末觸發的。該函式不穩定,因為無法預知在呼叫PulseEvent ()時哪些執行緒正處於等待狀態。
下面對這個觸發一個事件脈衝PulseEvent ()寫一個例子,主執行緒啟動7個子執行緒,其中有5個執行緒Sleep(10)後對一事件呼叫等待函式(稱為快執行緒),另有2個執行緒Sleep(100)後也對該事件呼叫等待函式(稱為慢執行緒)。主執行緒啟動所有子執行緒後再Sleep(50)保證有5個快執行緒都正處於等待狀態中。此時若主執行緒觸發一個事件脈衝,那麼對於手動置位事件,這5個執行緒都將順利執行下去。對於自動置位事件,這5個執行緒中會有中一個順利執行下去。而不論手動置位事件還是自動置位事件,那2個慢執行緒由於Sleep(100)所以會錯過事件脈衝,因此慢執行緒都會進入等待狀態而無法順利執行下去。
程式碼如下:
1. //使用PluseEvent()函式
2. #include <stdio.h>
3. #include <conio.h>
4. #include <process.h>
5. #include <windows.h>
6. HANDLE g_hThreadEvent;
7. //快執行緒
8. unsigned int __stdcall FastThreadFun(void *pPM)
9. {
10. Sleep(10); //用這個來保證各執行緒呼叫等待函式的次序有一定的隨機性
11. printf("%s 啟動\n", (PSTR)pPM);
12. WaitForSingleObject(g_hThreadEvent, INFINITE);
13. printf("%s 等到事件被觸發順利結束\n", (PSTR)pPM);
14. return 0;
15.}
16.//慢執行緒
17.unsigned int __stdcall SlowThreadFun(void *pPM)
18.{
19. Sleep(100);
20. printf("%s 啟動\n", (PSTR)pPM);
21. WaitForSingleObject(g_hThreadEvent, INFINITE);
22. printf("%s 等到事件被觸發順利結束\n", (PSTR)pPM);
23. return 0;
24.}
25.int main()
26.{
27. printf(" 使用PluseEvent()函式\n");
28. printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
29.
30. BOOL bManualReset = FALSE;
31. //建立事件第二個引數手動置位TRUE,自動置位FALSE
32. g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL);
33. if (bManualReset == TRUE)
34. printf("當前使用手動置位事件\n");
35. else
36. printf("當前使用自動置位事件\n");
37.
38. char szFastThreadName[5][30] = {"快執行緒1000", "快執行緒1001", "快執行緒1002", "快執行緒1003", "快執行緒1004"};
39. char szSlowThreadName[2][30] = {"慢執行緒196", "慢執行緒197"};
40.
41. int i;
42. for (i = 0; i < 5; i++)
43. _beginthreadex(NULL, 0, FastThreadFun, szFastThreadName[i], 0, NULL);
44. for (i = 0; i < 2; i++)
45. _beginthreadex(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, NULL);
46.
47. Sleep(50); //保證快執行緒已經全部啟動
48. printf("現在主執行緒觸發一個事件脈衝 - PulseEvent()\n");
49. PulseEvent(g_hThreadEvent);//呼叫PulseEvent()就相當於同時呼叫下面二句
50. //SetEvent(g_hThreadEvent);
51. //ResetEvent(g_hThreadEvent);
52.
53. Sleep(3000);
54. printf("時間到,主執行緒結束執行\n");
55. CloseHandle(g_hThreadEvent);
56. return 0;
57.}
對自動置位事件,執行結果如下:
對手動置位事件,執行結果如下:
最後總結下事件Event
1.事件是核心物件,事件分為手動置位事件和自動置位事件。事件Event內部它包含一個使用計數(所有核心物件都有),一個布林值表示是手動置位事件還是自動置位事件,另一個布林值用來表示事件有無觸發。
2.事件可以由SetEvent()來觸發,由ResetEvent()來設成未觸發。還可以由PulseEvent()來發出一個事件脈衝。
3.事件可以解決執行緒間同步問題,因此也能解決互斥問題。
3、《互斥量Mutex》
互斥量也是一個核心物件,它用來確保一個執行緒獨佔一個資源的訪問。互斥量與關鍵段的行為非常相似,並且互斥量可以用於不同程序中的執行緒互斥訪問資源。使用互斥量Mutex主要將用到四個函式。下面是這些函式的原型和使用說明。
第一個CreateMutex
函式功能:建立互斥量(注意與事件Event的建立函式對比)
函式原型:
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOLbInitialOwner,
LPCTSTRlpName
);
函式說明:
第一個引數表示安全控制,一般直接傳入NULL。
第二個引數用來確定互斥量的初始擁有者。如果傳入TRUE表示互斥量物件內部會記錄建立它的執行緒的執行緒ID號並將遞迴計數設定為1,由於該執行緒ID非零,所以互斥量處於未觸發狀態。如果傳入FALSE,那麼互斥量物件內部的執行緒ID號將設定為NULL,遞迴計數設定為0,這意味互斥量不為任何執行緒佔用,處於觸發狀態。
第三個引數用來設定互斥量的名稱,在多個程序中的執行緒就是通過名稱來確保它們訪問的是同一個互斥量。
函式訪問值:
成功返回一個表示互斥量的控制代碼,失敗返回NULL。
第二個開啟互斥量
函式原型:
HANDLEOpenMutex(
DWORDdwDesiredAccess,
BOOLbInheritHandle,
LPCTSTRlpName //名稱
);
函式說明:
第一個引數表示訪問許可權,對互斥量一般傳入MUTEX_ALL_ACCESS。詳細解釋可以檢視MSDN文件。
第二個引數表示互斥量控制代碼繼承性,一般傳入TRUE即可。
第三個引數表示名稱。某一個程序中的執行緒建立互斥量後,其它程序中的執行緒就可以通過這個函式來找到這個互斥量。
函式訪問值:
成功返回一個表示互斥量的控制代碼,失敗返回NULL。
第三個觸發互斥量
函式原型:
BOOLReleaseMutex (HANDLEhMutex)
函式說明:
訪問互斥資源前應該要呼叫等待函式,結束訪問時就要呼叫ReleaseMutex()來表示自己已經結束訪問,其它執行緒可以開始訪問了。
最後一個清理互斥量
由於互斥量是核心物件,因此使用CloseHandle()就可以(這一點所有核心物件都一樣)。
接下來我們就在經典多執行緒問題用互斥量來保證主執行緒與子執行緒之間的同步,由於互斥量的使用函式類似於事件Event,所以可以仿照上一篇的實現來寫出程式碼:
1. //經典執行緒同步問題互斥量Mutex
2. #include <stdio.h>
3. #include <process.h>
4. #include <windows.h>
5.
6. long g_nNum;
7. unsigned int __stdcall Fun(void *pPM);
8. const int THREAD_NUM = 10;
9. //互斥量與關鍵段
10.HANDLE g_hThreadParameter;
11.CRITICAL_SECTION g_csThreadCode;
12.
13.int main()
14.{
15. printf(" 經典執行緒同步互斥量Mutex\n");
16. printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");