第一部分:線程同步之關鍵段
1、本文首先介紹下如何使用關鍵段,然後再深層次的分析下關鍵段的實現機制與原理。
定義關鍵段變量:CRITICAL_SECTION g_csThreadParameter, g_csThreadCode;
關鍵段CRITICAL_SECTION一共就四個函數,使用很是方便。下面是這四個函數的原型和使用說明。
1)函數原型:void InitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函數說明:定義關鍵段變量後必須先初始化。
2)函數原型:void DeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函數說明:用完之後記得銷燬。
3)函數原型:void EnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函數說明:系統保證各線程互斥的進入關鍵段區域。
4)函數原型:void LeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection);
函數說明:離開關鍵段區域。
2、在經典多線程問題中設置二個關鍵區域。一個是主線程在遞增子線程序號時,另一個是各子線程互斥的訪問輸出全局資源時。詳見代碼:
運行結果如下圖:
可以看出來,各子線程已經可以互斥的訪問與輸出全局資源了,但主線程與子線程之間的同步還是有點問題。這是爲什麼了?
要解開這個迷,最直接的方法就是先在程序中加上斷點來查看程序的運行流程。斷點處置示意如下:
然後按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; 即擁有該關鍵段的線程句柄。
第五個參數: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.推薦關鍵段與旋轉鎖配合使用。
文章轉載於:http://blog.csdn.net/morewindows/article/details/7442639
--------------------------------------------------------------------------------------------第二部分:臨界區
關於臨界區的觀念,一般操作系統書上面都有。
適用範圍:它只能同步一個進程中的線程,不能跨進程同步。一般用它來做單個進程內的代碼快同步,效率比較高
windows中與臨界區有關的結構是 CRITICAL_SECTION,關於該結構體的內部結構可參考here
使用時,主線程中要先初始化臨界區,最後要刪除臨界區,具體使用見下面代碼:
從一個例子來說明:假設有三個線程都需要使用打印機,我們可以把打印的代碼放到臨界區,這樣就可以保證每次只有一個線程在使用打印機。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
#include<string> #include<iostream> #include<process.h> #include<windows.h> using namespace std; //定義一個臨界區 CRITICAL_SECTION
g_cs; //線程綁定的函數返回值和參數是確定的,而且一定要__stdcall unsigned
__stdcall threadFun( void *param) { EnterCriticalSection(&g_cs); //進入臨界區,如果有其他線程則等待 cout<<*(string
*)(param)<<endl; LeaveCriticalSection(&g_cs); //退出臨界區,其他線程可以進來了 return 1; } int main() { //初始化臨界區 InitializeCriticalSection(&g_cs); HANDLE hth1,
hth2, hth3; string
s1 = "first" ,
s2 = "second" ,
s3 = "third" ; //創建線程 hth1
= ( HANDLE )_beginthreadex(NULL,
0, threadFun, &s1, 0, NULL); hth2
= ( HANDLE )_beginthreadex(NULL,
0, threadFun, &s2, 0, NULL); hth3
= ( HANDLE )_beginthreadex(NULL,
0, threadFun, &s3, 0, NULL); //等待子線程結束 WaitForSingleObject(hth1,
INFINITE); WaitForSingleObject(hth2,
INFINITE); WaitForSingleObject(hth3,
INFINITE); //一定要記得關閉線程句柄 CloseHandle(hth1); CloseHandle(hth2); CloseHandle(hth3); //刪除臨界區 DeleteCriticalSection(&g_cs); } |
再看另外一個問題:編寫一個程序,開啓3個線程,這3個線程的ID分別爲A、B、C,每個線程將自己的ID在屏幕上打印10遍,要求輸出結果必須按ABC的順序顯示;如:ABCABC….依次遞推, 仿照文章windows多線程同步--信號量中的代碼,我們把信號量替換成臨界區。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
|
#include<string> #include<iostream> #include<process.h> #include<windows.h> using namespace std; //聲明3個臨界區 CRITICAL_SECTION
g_cs1, g_cs2, g_cs3; //線程綁定的函數返回值和參數是確定的,而且一定要__stdcall unsigned
__stdcall threadFunA( void *) { for ( int i
= 0; i < 10; i++){ EnterCriticalSection(&g_cs1); //進入臨界區 cout<< "A" ; LeaveCriticalSection(&g_cs2); //離開臨界區 } return 1; } unsigned
__stdcall threadFunB( void *) { for ( int i
= 0; i < 10; i++){ EnterCriticalSection(&g_cs2); //進入臨界區 cout<< "B" ; LeaveCriticalSection(&g_cs3); //離開臨界區 } return 2; } unsigned
__stdcall threadFunC( void *) { for ( int i
= 0; i < 10; i++){ EnterCriticalSection(&g_cs3); //進入臨界區 cout<< "C" ; LeaveCriticalSection(&g_cs1); //離開臨界區 } return 3; } int main() { //初始化臨界區 InitializeCriticalSection(&g_cs1); InitializeCriticalSection(&g_cs2); InitializeCriticalSection(&g_cs3); HANDLE hth1,
hth2, hth3; //創建線程 hth1
= ( HANDLE )_beginthreadex(NULL,
0, threadFunA, NULL, 0, NULL); hth2
= ( HANDLE )_beginthreadex(NULL,
0, threadFunB, NULL, 0, NULL); hth3
= ( HANDLE )_beginthreadex(NULL,
0, threadFunC, NULL, 0, NULL); //等待子線程結束 WaitForSingleObject(hth1,
INFINITE); WaitForSingleObject(hth2,
INFINITE); WaitForSingleObject(hth3,
INFINITE); //一定要記得關閉線程句柄 CloseHandle(hth1); CloseHandle(hth2); CloseHandle(hth3); //刪除臨界區 DeleteCriticalSection(&g_cs1); DeleteCriticalSection(&g_cs2); DeleteCriticalSection(&g_cs3); } |
爲什麼會這樣呢,因爲臨界區有所有權的概念,即某個線程進入臨界區後,就擁有該臨界區的所有權,在他離開臨界區之前,他可以無限次的再次進入該臨界區,上例中線程A獲得臨界區1的所有權後,在線程C調用LeaveCriticalSection(&g_cs1)之前,A是可以無限次的進入臨界區1的。利用信號量之所以可以實現題目的要求,是因爲信號量沒有所有權的概念,某個線程獲得信號量後,如果信號量的值爲0,那麼他一定要等到信號量被釋放時,才能再次獲得