C加加線程同步的四種方式
線程之間通信的兩個基本問題是互斥和同步。
(1)線程同步是指線程之間所具有的一種制約關係,一個線程的執行依賴另一個線程的消息,當它沒有得到另一個線程的消息時應等待,直到消息到達時才被喚醒。
(2)線程互斥是指對於共享的操作系統資源(指的是廣義的」資源」,而不是Windows的.res文件,譬如全局變數就是一種共享資源),在各線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任何時刻最多只允許一個線程去使用,其它要使用該資源的線程必須等待,直到佔用資源者釋放該資源。
線程互斥是一種特殊的線程同步。實際上,互斥和同步對應著線程間通信發生的兩種情況:
(1)當有多個線程訪問共享資源而不使資源被破壞時;
(2)當一個線程需要將某個任務已經完成的情況通知另外一個或多個線程時。
想要一起學習C++的可以加裙二四八八九四四三零,裙內有各種資料滿足大家,歡迎加裙
從大的方面講,線程的同步可分用戶模式的線程同步和內核對象的線程同步兩大類。用戶模式中線程的同步方法主要有原子訪問和臨界區等方法。其特點是同步速度特別快,適合於對線程運行速度有嚴格要求的場合。
內核對象的線程同步則主要由 事件 、 等待定時器 、 信號量 以及 信號燈 等內核對象構成。由於這種同步機制使用了內核對象,使用時必須將線程從用戶模式切換到內核模式,而這種轉換一般要耗費近千個CPU周期,因此同步速度較慢,但在適用性上卻要遠優於用戶模式的線程同步方式。
在WIN32中,同步機制主要有以下幾種:
(1)事件(Event);
(2)信號量(semaphore);
(3)互斥量(mutex);
(4)臨界區(Critical section)。
臨界區
臨界區(Critical Section)是一段獨佔對某些共享資源訪問的代碼,在任意時刻只允許一個線程對共享資源進行訪問。如果有多個線程試圖同時訪問臨界區,那麼在有一個線程進入後其他所有試圖訪問此臨界區的線程將被掛起,並一直持續到進入臨界區的線程離開。臨界區在被釋放後,其他線程可以繼續搶佔,並以此達到用原子方式操作共享資源的目的。
臨界區在使用時以CRITICAL_SECTION結構對象保護共享資源,並分別用EnterCriticalSection()和LeaveCriticalSection()函數去標識和釋放一個臨界區。所用到的CRITICAL_SECTION結構對象必須經過InitializeCriticalSection()的初始化後才能使用,而且必須確保所有線程中的任何試圖訪問此共享資源的代碼都處在此臨界區的保護之下。否則臨界區將不會起到應有的作用,共享資源依然有被破壞的可能。
全局變數
因為進程中的所有線程均可以訪問所有的全局變數,因而全局變數成為Win32多線程通信的最簡單方式。例如:
int var; //全局變數
UINTThreadFunction(LPVOIDpParam)
{
var = 0;
while (var
{
//線程處理
::InterlockedIncrement(long*) &var);
}
return 0;
}
請看下列程序:
int globalFlag = false;
DWORDWINAPIThreadFunc(LPVOID n)
{
Sleep(2000);
globalFlag = true;
return 0;
}
int main()
{
HANDLEhThrd;
DWORDthreadId;
hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
if (hThrd)
{
printf("Thread launched
");
CloseHandle(hThrd);
}
while (!globalFlag)
;
printf("exit
");
}
上述程序中使用全局變數和while循環查詢進行線程間同步,實際上,這是一種應該避免的方法,因為:
(1)當主線程必須使自己與ThreadFunc函數的完成運行實現同步時,它並沒有使自己進入睡眠狀態。由於主線程沒有進入睡眠狀態,因此操作系統繼續為它調度C P U時間,這就要佔用其他線程的寶貴時間周期;
(2)當主線程的優先順序高於執行ThreadFunc函數的線程時,就會發生globalFlag永遠不能被賦值為true的情況。因為在這種情況下,系統決不會將任何時間片分配給ThreadFunc線程。
事件
事件(Event)是WIN32提供的最靈活的線程間同步方式,事件可以處於激髮狀態(signaled or true)或未激髮狀態(unsignal or false)。根據狀態變遷方式的不同,事件可分為兩類:
(1)手動設置:這種對象只可能用程序手動設置,在需要該事件或者事件發生時,採用SetEvent及ResetEvent來進行設置。
(2)自動恢復:一旦事件發生並被處理後,自動恢復到沒有事件狀態,不需要再次設置。
使用」事件」機制應注意以下事項:
(1)如果跨進程訪問事件,必須對事件命名,在對事件命名的時候,要注意不要與系統命名空間中的其它全局命名對象衝突;
(2)事件是否要自動恢復;
(3)事件的初始狀態設置。
由於event對象屬於內核對象,故進程B可以調用OpenEvent函數通過對象的名字獲得進程A中event對象的句柄,然後將這個句柄用於ResetEvent、SetEvent和WaitForMultipleObjects等函數中。此法可以實現一個進程的線程式控制制另一進程中線程的運行,例如:
HANDLEhEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);
信號量
信號量是維護0到指定最大值之間的同步對象。信號量狀態在其計數大於0時是有信號的,而其計數是0時是無信號的。信號量對象在控制上可以支持有限數量共享資源的訪問。
信號量的特點和用途可用下列幾句話定義:
(1)如果當前資源的數量大於0,則信號量有效;
(2)如果當前資源數量是0,則信號量無效;
(3)系統決不允許當前資源的數量為負值;
(4)當前資源數量決不能大於最大資源數量。
創建信號量
HANDLECreateSemaphore (
PSECURITY_ATTRIBUTEpsa,
LONG lInitialCount, //開始時可供使用的資源數
LONG lMaximumCount, //最大資源數
PCTSTRpszName);
釋放信號量
通過調用ReleaseSemaphore函數,線程就能夠對信標的當前資源數量進行遞增,該函數原型為:
BOOL WINAPIReleaseSemaphore(
HANDLEhSemaphore,
LONG lReleaseCount, //信號量的當前資源數增加lReleaseCount
LPLONGlpPreviousCount
);
打開信號量
和其他核心對象一樣,信號量也可以通過名字跨進程訪問,打開信號量的API為:
HANDLEOpenSemaphore (
DWORDfdwAccess,
BOOL bInherithandle,
PCTSTRpszName
);
互鎖訪問
當必須以原子操作方式來修改單個值時,互鎖訪問函數是相當有用的。所謂原子訪問,是指線程在訪問資源時能夠確保所有其他線程都不在同一時間內訪問相同的資源。
請看下列代碼:
int globalVar = 0;
DWORDWINAPIThreadFunc1(LPVOID n)
{
globalVar++;
return 0;
}
DWORDWINAPIThreadFunc2(LPVOID n)
{
globalVar++;
return 0;
}
運行ThreadFunc1和ThreadFunc2線程,結果是不可預料的,因為globalVar++並不對應著一條機器指令,我們看看globalVar++的反彙編代碼:
0040103D addeax,1
在」mov eax,[globalVar (0042d3f0)]」 指令與」add eax,1″ 指令以及」add eax,1″ 指令與」mov [globalVar (0042d3f0)],eax」指令之間都可能發生線程切換,使得程序的執行後globalVar的結果不能確定。我們可以使用InterlockedExchangeAdd函數解決這個問題:
int globalVar = 0;
DWORDWINAPIThreadFunc1(LPVOID n)
{
InterlockedExchangeAdd(&globalVar,1);
return 0;
}
DWORDWINAPIThreadFunc2(LPVOID n)
{
InterlockedExchangeAdd(&globalVar,1);
return 0;
}
InterlockedExchangeAdd保證對變數globalVar的訪問具有」原子性」。互鎖訪問的控制速度非常快,調用一個互鎖函數的CPU周期通常小於50,不需要進行用戶方式與內核方式的切換(該切換通常需要運行1000個CPU周期)。
互鎖訪問函數的缺點在於其只能對單一變數進行原子訪問,如果要訪問的資源比較複雜,仍要使用臨界區或互斥。
可等待定時器
可等待定時器是在某個時間或按規定的間隔時間發出自己的信號通知的內核對象。它們通常用來在某個時間執行某個操作。
創建可等待定時器
HANDLECreateWaitableTimer(
PSECURITY_ATTRISUTESpsa,
BOOL fManualReset,//人工重置或自動重置定時器
PCTSTRpszName);
設置可等待定時器
可等待定時器對象在非激活狀態下被創建,程序員應調用 SetWaitableTimer函數來界定定時器在何時被激活:
BOOL SetWaitableTimer(
HANDLEhTimer, //要設置的定時器
const LARGE_INTEGER *pDueTime, //指明定時器第一次激活的時間
LONG lPeriod, //指明此後定時器應該間隔多長時間激活一次
PTIMERAPCROUTINEpfnCompletionRoutine,
PVOIDPvArgToCompletionRoutine,
BOOL fResume);
取消可等待定時器
BOOL CancelWaitableTimer(
HANDLEhTimer //要取消的定時器
);
打開可等待定時器
作為一種內核對象,WaitableTimer也可以被其他進程以名字打開:
HANDLEOpenWaitableTimer (
DWORDfdwAccess,
BOOL bInherithandle,
PCTSTRpszName
);
實例
下面給出的一個程序可能發生死鎖現象:
#include
#include
CRITICAL_SECTIONcs1, cs2;
long WINAPIThreadFn(long);
main()
{
long iThreadID;
InitializeCriticalSection(&cs1);
InitializeCriticalSection(&cs2);
CloseHandle(CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFn, NULL, 0,&iThreadID));
while (TRUE)
{
EnterCriticalSection(&cs1);
printf("
線程1佔用臨界區1");
EnterCriticalSection(&cs2);
printf("
線程1佔用臨界區2");
printf("
線程1佔用兩個臨界區");
LeaveCriticalSection(&cs2);
LeaveCriticalSection(&cs1);
printf("
線程1釋放兩個臨界區");
Sleep(20);
};
return (0);
}
long WINAPIThreadFn(long lParam)
{
while (TRUE)
{
EnterCriticalSection(&cs2);
printf("
線程2佔用臨界區2");
EnterCriticalSection(&cs1);
printf("
線程2佔用臨界區1");
printf("
線程2佔用兩個臨界區");
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs2);
printf("
線程2釋放兩個臨界區");
Sleep(20);
};
}
運行這個程序,在中途一旦發生這樣的輸出:
線程1佔用臨界區1 線程2佔用臨界區2
或
線程2佔用臨界區2 線程1佔用臨界區1
或
線程1佔用臨界區2 線程2佔用臨界區1
或
線程2佔用臨界區1 線程1佔用臨界區2
程序就」死」掉了,再也運行不下去。因為這樣的輸出,意味著兩個線程相互等待對方釋放臨界區,也即出現了死鎖。
如果我們將線程2的控制函數改為:
long WINAPIThreadFn(long lParam)
{
while (TRUE)
{
EnterCriticalSection(&cs1);
printf("
線程2佔用臨界區1");
EnterCriticalSection(&cs2);
printf("
線程2佔用臨界區2");
printf("
線程2佔用兩個臨界區");
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs2);
printf("
線程2釋放兩個臨界區");
Sleep(20);
};
}
再次運行程序,死鎖被消除,程序不再擋掉。這是因為我們改變了線程2中獲得臨界區1、2的順序,消除了線程1、2相互等待資源的可能性。
由此我們得出結論,在使用線程間的同步機制時,要特別留心死鎖的發生。
想要一起學習C++的可以加裙二四八八九四四三零,裙內有各種資料滿足大家,歡迎加裙


※用C語言寫面向的對象是一種什麼樣的體驗
※C語言的趣味題目學習
※現代C加加函數式編程
※給那些初學C語言的朋友,手把手教做一個簡單的人機猜數遊戲
※C語言學習之什麼是指針
TAG:C加加 |
※七大進程間通信和線程同步
※蘋果加入線程組 將加快智能家居領域腳步?
※進程與線程的區別
※線程,進程,程序的區別
※盤點嵌入式Linux中進程間通信和線程間通信的幾種方式
※淺析幾種線程安全模型
※流言終結者系列:第三代銳龍關同步多線程能增加遊戲幀數?
※從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理
※AMD新一代處理器測試,單線程大幅提升
※如何設計一個實用的線程池?
※常駐線程是一種什麼體驗
※一個故事講完進程、線程和協程
※線程池定製初探
※英特爾處理器是六核六線程好,還是四核八線程好?
※多線程中的兩個問題探討
※同步容器(如Vector)並不是所有操作都線程安全
※線程中斷以及線程中斷引發的那些問題
※雙管齊下,AMD開啟線程撕裂者暴走模式!
※電腦CPU的八線程、超線程技術,指的是什麼?
※為什麼CPU線程數最多是核心數的一倍,而不能有四核十六線程?