windows系统c++多线程开发
線程的一些基本概念
一、線程的基本概念。
基本概念:線程,即輕量級進程(LWP:LightWeight Process),是程序執行流的最小單元。一個標準的線程由線程ID、當前指令指針(PC),寄存器集合和堆棧組成。線程是進程中的一個實體,是被系統獨立調度和分派的基本單位。線程不擁有系統資源,近擁有少量運行必須的資源。
二、線程的基本狀態。
基本狀態:就緒、阻塞和運行三種基本狀態。
就緒狀態,指線程具備運行的所有條件,邏輯上可以運行,在等待處理機;
運行狀態,指線程占有處理機正在運行;
阻塞狀態,指線程在等待一個事件(如信號量),邏輯上不可執行。
三、進程和線程的關系。
簡而言之,一個程序至少有一個進程,一個進程至少有一個線程.
進程和線程的主要差別在于它們是不同的操作系統資源管理方式。進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等于整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對于一些要求同時進行并且又要共享某些變量的并發操作,只能用線程,不能用進程。
四、線程同步互斥的4種方式
1.??臨界區(Critical Section):適合一個進程內的多線程訪問公共區域或代碼段時使用
2.??互斥量 (Mutex):適合不同進程內多線程訪問公共區域或代碼段時使用,與臨界區相似。
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);
?
然后在經典多線程問題中設置二個關鍵區域。一個是主線程在遞增子線程序號時,另一個是各子線程互斥的訪問輸出全局資源時。詳見代碼:
[cpp]?view plain?copy
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()就可以完成清理與銷毀了。
?
在經典多線程問題中設置一個事件和一個關鍵段。用事件處理主線程與子線程的同步,用關鍵段來處理各子線程間的互斥。詳見代碼:
[cpp]?view plain?copy
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)所以會錯過事件脈沖,因此慢線程都會進入等待狀態而無法順利執行下去。
代碼如下:
[cpp]?view plain?copy
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,所以可以仿照上一篇的實現來寫出代碼:
[cpp]?view plain?copy
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");??
17.??????
18.????//初始化互斥量與關鍵段?第二個參數為TRUE表示互斥量為創建線程所有??
19.????g_hThreadParameter?=?CreateMutex(NULL,?FALSE,?NULL);??
20.????InitializeCriticalSection(&g_csThreadCode);??
21.??
22.????HANDLE??handle[THREAD_NUM];???
23.????g_nNum?=?0;???
24.????int?i?=?0;??
25.????while?(i?<?THREAD_NUM)???
26.????{??
27.????????handle[i]?=?(HANDLE)_beginthreadex(NULL,?0,?Fun,?&i,?0,?NULL);??
28.????????WaitForSingleObject(g_hThreadParameter,?INFINITE);?//等待互斥量被觸發??
29.????????i++;??
30.????}??
31.????WaitForMultipleObjects(THREAD_NUM,?handle,?TRUE,?INFINITE);??
32.??????
33.????//銷毀互斥量和關鍵段??
34.????CloseHandle(g_hThreadParameter);??
35.????DeleteCriticalSection(&g_csThreadCode);??
36.????for?(i?=?0;?i?<?THREAD_NUM;?i++)??
37.????????CloseHandle(handle[i]);??
38.????return?0;??
39.}??
40.unsigned?int?__stdcall?Fun(void?*pPM)??
41.{??
42.????int?nThreadNum?=?*(int?*)pPM;??
43.????ReleaseMutex(g_hThreadParameter);//觸發互斥量??
44.??????
45.????Sleep(50);//some?work?should?to?do??
46.??
47.????EnterCriticalSection(&g_csThreadCode);??
48.????g_nNum++;??
49.????Sleep(0);//some?work?should?to?do??
50.????printf("線程編號為%d??全局資源值為%d\n",?nThreadNum,?g_nNum);??
51.????LeaveCriticalSection(&g_csThreadCode);??
52.????return?0;??
53.}??
運行結果如下圖:
可以看出,與關鍵段類似,互斥量也是不能解決線程間的同步問題。
???????聯想到關鍵段會記錄線程ID即有“線程擁有權”的,而互斥量也記錄線程ID,莫非它也有“線程擁有權”這一說法。
???????答案確實如此,互斥量也是有“線程擁有權”概念的。“線程擁有權”在關鍵段中有詳細的說明,這里就不再贅述了。另外由于互斥量常用于多進程之間的線程互斥,所以它比關鍵段還多一個很有用的特性——“遺棄”情況的處理。比如有一個占用互斥量的線程在調用ReleaseMutex()觸發互斥量前就意外終止了(相當于該互斥量被“遺棄”了),那么所有等待這個互斥量的線程是否會由于該互斥量無法被觸發而陷入一個無窮的等待過程中了?這顯然不合理。因為占用某個互斥量的線程既然終止了那足以證明它不再使用被該互斥量保護的資源,所以這些資源完全并且應當被其它線程來使用。因此在這種“遺棄”情況下,系統自動把該互斥量內部的線程ID設置為0,并將它的遞歸計數器復置為0,表示這個互斥量被觸發了。然后系統將“公平地”選定一個等待線程來完成調度(被選中的線程的WaitForSingleObject()會返回WAIT_ABANDONED_0)。
?
下面寫二個程序來驗證下:
第一個程序創建互斥量并等待用戶輸入后就觸發互斥量。第二個程序先打開互斥量,成功后就等待并根據等待結果作相應的輸出。詳見代碼:
第一個程序:
[cpp]?view plain?copy
1.?#include?<stdio.h>??
2.?#include?<conio.h>??
3.?#include?<windows.h>??
4.?const?char?MUTEX_NAME[]?=?"Mutex_MoreWindows";??
5.?int?main()??
6.?{??
7.?????HANDLE?hMutex?=?CreateMutex(NULL,?TRUE,?MUTEX_NAME);?//創建互斥量??
8.?????printf("互斥量已經創建,現在按任意鍵觸發互斥量\n");??
9.?????getch();??
10.????//exit(0);??
11.????ReleaseMutex(hMutex);??
12.????printf("互斥量已經觸發\n");??
13.????CloseHandle(hMutex);??
14.????return?0;??
15.}??
第二個程序:
[cpp]?view plain?copy
1.?#include?<stdio.h>??
2.?#include?<windows.h>??
3.?const?char?MUTEX_NAME[]?=?"Mutex_MoreWindows";??
4.?int?main()??
5.?{??
6.?????HANDLE?hMutex?=?OpenMutex(MUTEX_ALL_ACCESS,?TRUE,?MUTEX_NAME);?//打開互斥量??
7.?????if?(hMutex?==?NULL)??
8.?????{??
9.?????????printf("打開互斥量失敗\n");??
10.????????return?0;??
11.????}??
12.????printf("等待中....\n");??
13.????DWORD?dwResult?=?WaitForSingleObject(hMutex,?20?*?1000);?//等待互斥量被觸發??
14.????switch?(dwResult)??
15.????{??
16.????case?WAIT_ABANDONED:??
17.????????printf("擁有互斥量的進程意外終止\n");??
18.????????break;??
19.??
20.????case?WAIT_OBJECT_0:??
21.????????printf("已經收到信號\n");??
22.????????break;??
23.??
24.????case?WAIT_TIMEOUT:??
25.????????printf("信號未在規定的時間內送到\n");??
26.????????break;??
27.????}??
28.????CloseHandle(hMutex);??
29.????return?0;??
30.}??
運用這二個程序時要先啟動程序一再啟動程序二。下面展示部分輸出結果:
結果一.二個進程順利執行完畢:
結果二.將程序一中//exit(0);前面的注釋符號去掉,這樣程序一在觸發互斥量之前就會因為執行exit(0);語句而且退出,程序二會收到WAIT_ABANDONED消息并輸出“擁有互斥量的進程意外終止”:
有這個對“遺棄”問題的處理,在多進程中的線程同步也可以放心的使用互斥量。
?
最后總結下互斥量Mutex:
1.互斥量是內核對象,它與關鍵段都有“線程所有權”所以不能用于線程的同步。
2.互斥量能夠用于多個進程之間線程互斥問題,并且能完美的解決某進程意外終止所造成的“遺棄”問題。
4、《信號量Semaphone》
首先也來看看如何使用信號量,信號量Semaphore常用有三個函數,使用很方便。下面是這幾個函數的原型和使用說明。
第一個?CreateSemaphore
函數功能:創建信號量
函數原型:
HANDLE?CreateSemaphore(
??LPSECURITY_ATTRIBUTES?lpSemaphoreAttributes,
??LONG?lInitialCount,
??LONG?lMaximumCount,
??LPCTSTR?lpName
);
函數說明:
第一個參數表示安全控制,一般直接傳入NULL。
第二個參數表示初始資源數量。
第三個參數表示最大并發數量。
第四個參數表示信號量的名稱,傳入NULL表示匿名信號量。
?
第二個?OpenSemaphore
函數功能:打開信號量
函數原型:
HANDLE?OpenSemaphore(
??DWORD?dwDesiredAccess,
??BOOL?bInheritHandle,
??LPCTSTR?lpName
);
函數說明:
第一個參數表示訪問權限,對一般傳入SEMAPHORE_ALL_ACCESS。詳細解釋可以查看MSDN文檔。
第二個參數表示信號量句柄繼承性,一般傳入TRUE即可。
第三個參數表示名稱,不同進程中的各線程可以通過名稱來確保它們訪問同一個信號量。
?
第三個?ReleaseSemaphore
函數功能:遞增信號量的當前資源計數
函數原型:
BOOL?ReleaseSemaphore(
??HANDLE?hSemaphore,
??LONG?lReleaseCount,??
??LPLONG?lpPreviousCount?
);
函數說明:
第一個參數是信號量的句柄。
第二個參數表示增加個數,必須大于0且不超過最大資源數量。
第三個參數可以用來傳出先前的資源計數,設為NULL表示不需要傳出。
?
注意:當前資源數量大于0,表示信號量處于觸發,等于0表示資源已經耗盡故信號量處于末觸發。在對信號量調用等待函數時,等待函數會檢查信號量的當前資源計數,如果大于0(即信號量處于觸發狀態),減1后返回讓調用線程繼續執行。一個線程可以多次調用等待函數來減小信號量。?
?
最后一個信號量的清理與銷毀
由于信號量是內核對象,因此使用CloseHandle()就可以完成清理與銷毀了。
?
在經典多線程問題中設置一個信號量和一個關鍵段。用信號量處理主線程與子線程的同步,用關鍵段來處理各子線程間的互斥。詳見代碼:
[cpp]?view plain?copy
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_hThreadParameter;??
9.?CRITICAL_SECTION??g_csThreadCode;??
10.int?main()??
11.{??
12.????printf("?????經典線程同步?信號量Semaphore\n");??
13.????printf("?--?by?MoreWindows(?http://blog.csdn.net/MoreWindows?)?--\n\n");??
14.??
15.????//初始化信號量和關鍵段??
16.????g_hThreadParameter?=?CreateSemaphore(NULL,?0,?1,?NULL);//當前0個資源,最大允許1個同時訪問??
17.????InitializeCriticalSection(&g_csThreadCode);??
18.??
19.????HANDLE??handle[THREAD_NUM];???
20.????g_nNum?=?0;??
21.????int?i?=?0;??
22.????while?(i?<?THREAD_NUM)???
23.????{??
24.????????handle[i]?=?(HANDLE)_beginthreadex(NULL,?0,?Fun,?&i,?0,?NULL);??
25.????????WaitForSingleObject(g_hThreadParameter,?INFINITE);//等待信號量>0??
26.????????++i;??
27.????}??
28.????WaitForMultipleObjects(THREAD_NUM,?handle,?TRUE,?INFINITE);??
29.??????
30.????//銷毀信號量和關鍵段??
31.????DeleteCriticalSection(&g_csThreadCode);??
32.????CloseHandle(g_hThreadParameter);??
33.????for?(i?=?0;?i?<?THREAD_NUM;?i++)??
34.????????CloseHandle(handle[i]);??
35.????return?0;??
36.}??
37.unsigned?int?__stdcall?Fun(void?*pPM)??
38.{??
39.????int?nThreadNum?=?*(int?*)pPM;??
40.????ReleaseSemaphore(g_hThreadParameter,?1,?NULL);//信號量++??
41.??
42.????Sleep(50);//some?work?should?to?do??
43.??
44.????EnterCriticalSection(&g_csThreadCode);??
45.????++g_nNum;??
46.????Sleep(0);//some?work?should?to?do??
47.????printf("線程編號為%d??全局資源值為%d\n",?nThreadNum,?g_nNum);??
48.????LeaveCriticalSection(&g_csThreadCode);??
49.????return?0;??
50.}??
運行結果如下圖:
可以看出來,信號量也可以解決線程之間的同步問題。
?
由于信號量可以計算資源當前剩余量并根據當前剩余量與零比較來決定信號量是處于觸發狀態或是未觸發狀態,因此信號量的應用范圍相當廣泛。
?
五、多線程中棧與堆是公有的還是私有的。
在多線程環境下,每個線程擁有一個棧和一個程序計數器。棧和程序計數器用來保存線程的執行歷史和線程的執行狀態,是線程私有的資源。其他的資源(比如堆、地址空間、全局變量)是由同一個進程內的多個線程共享。
六、在Windows編程中互斥量與臨界區比較類似,請分析一下二者的主要區別。
臨界區不能用于進程間同步, 它不屬于內核對象, 所以速度比較快;
互斥量可以用于進程間同步, 屬于內核對象, 速度較慢。
1、????臨界區只能用于對象在同一進程里線程間的互斥訪問;互斥體可以用于對象進程間或線程間的互斥訪問;
2、????臨界區是非內核對象,只在用戶態進行鎖操作,速度快;
3、????互斥體是內核對象,在核心態進行鎖操作,速度慢。
4、????臨界區和互斥體在Windows平臺都下可用;Linux下只有互斥體可用。
七、多線程中多參數傳遞。
?我們先來看一個簡單的程序:
[cpp]?view plain?copy
?
1.?#include?<stdio.h>??
2.?#include?<windows.h>??
3.???
4.?DWORD?WINAPI?ThreadFun(LPVOID?pM)??
5.?{??
6.?????printf("%s\n",?pM);??
7.?????return?0;??
8.?}??
9.???
10.int?main()??
11.{??
12.????printf("main?thread\n");??
13.????HANDLE?handle?=?CreateThread(NULL,?0,?ThreadFun,?"hello?world",?0,?NULL);??
14.????CloseHandle(handle);??
15.??
16.????Sleep(2000);??
17.??
18.????return?0;??
19.}??
? ? ? 在主線程中,傳遞給線程函數ThreadFun的串為"helloworld".受CreateThread函數的限制,只能傳遞LPVOID,糟糕了,如果你要傳遞很多東東給子線程,那么,該怎么辦呢?請看下面的程序:
[cpp]?view plain?copy
?
1.?#include?<stdio.h>??
2.?#include?<windows.h>??
3.???
4.?typedef?struct?test??
5.?{??
6.?????int?n;??
7.?????char?c;??
8.?????char?a[100];??
9.?}Test;??
10.??
11.DWORD?WINAPI?ThreadFun(LPVOID?pM)??
12.{??
13.????Test?*pt?=?(Test?*)pM;??
14.????printf("%d,?%c,?%s\n",?pt->n,?pt->c,?pt->a);??
15.????return?0;??
16.}??
17.??
18.int?main()??
19.{??
20.????printf("main?thread\n");??
21.??
22.????Test?t;??
23.????t.n?=?1;??
24.????t.c?=?'x';??
25.????memset(t.a,?0,?sizeof(t.a));??
26.????strncpy(t.a,?"so?great",?sizeof(t.a)?-?1);??
27.????HANDLE?handle?=?CreateThread(NULL,?0,?ThreadFun,?(LPVOID)&t,?0,?NULL);??
28.????CloseHandle(handle);??
29.??
30.????Sleep(2000);??
31.??
32.????return?0;??
33.}??
? ? ? 這種方式,無處不在,無處不見。下次見了,要認識哈。
?
?
總結
以上是生活随笔為你收集整理的windows系统c++多线程开发的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 莫烦python_莫烦python教学网
- 下一篇: QT_T04-COOD