簡介程序員
線程之間通訊的兩個基本問題是互斥和同步。編程
線程同步是指線程之間所具備的一種制約關係,一個線程的執行依賴另外一個線程的消息,當它沒有獲得另外一個線程的消息時應等待,直到消息到達時才被喚醒。安全
線程互斥是指對於共享的操做系統資源(指的是廣義的"資源",而不是Windows的.res文件,譬如全局變量就是一種共享資源),在各線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任什麼時候刻最多隻容許一個線程去使用,其它要使用該資源的線程必須等待,直到佔用資源者釋放該資源。多線程
線程互斥是一種特殊的線程同步。函數
實際上,互斥和同步對應着線程間通訊發生的兩種狀況:性能
(1)
當有多個線程訪問共享資源而不使資源被破壞時;操作系統
(2)當一個線程須要將某個任務已經完成的狀況
通知
另一個或多個線程時。線程
在
WIN32
中,同步機制主要有如下幾種:指針
(1)事件(Event);對象
(2)信號量(semaphore);
(3)互斥量(mutex);
(4)臨界區(
Critical
section)。
全局變量
由於進程中的全部線程都可以訪問全部的全局變量,於是全局變量成爲Win32多線程通訊的最簡單方式。例如:
int var; //全局變量
UINT ThreadFunction(LPVOIDpParam)
{
var = 0;
while (var < MaxValue)
{
//線程處理
::InterlockedIncrement(long*) &var);
}
return 0;
}
請看下列程序:
int globalFlag = false;
DWORD WINAPI ThreadFunc(LPVOID n)
{
Sleep(2000);
globalFlag = true;
return 0;
}
int main()
{
HANDLE hThrd;
DWORD threadId;
hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId);
if (hThrd)
{
printf("Thread launched/n");
CloseHandle(hThrd);
}
while (!globalFlag)
;
printf("exit/n");
}
上述程序中使用全局變量和while循環查詢進行線程間同步,實際上,這是一種應該避免的方法,由於:
(1)當主線程必須使本身與ThreadFunc函數的完成運行實現同步時,它並無使本身進入睡眠狀態。因爲主線程沒有進入睡眠狀態,所以操做系統繼續爲它調度C P U時間,這就要佔用其餘線程的寶貴時間週期;
(2)當主線程的優先級高於執行ThreadFunc函數的線程時,就會發生globalFlag永遠不能被賦值爲true的狀況。由於在這種狀況下,系統決不會將任什麼時候間片分配給ThreadFunc線程。
事件
事件(Event)是WIN32提供的最靈活的線程間同步方式,事件能夠處於激發狀態(signaled or true)或未激發狀態(unsignal or false)。根據狀態變遷方式的不一樣,事件可分爲兩類:
(1)手動設置:這種對象只可能用程序手動設置,在須要該事件或者事件發生時,採用SetEvent及ResetEvent來進行設置。
(2)自動恢復:一旦事件發生並被處理後,自動恢復到沒有事件狀態,不須要再次設置。
建立事件的函數原型爲:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
//
SECURITY
_ATTRIBUTES結構指針,可爲NULL
BOOL bManualReset,
// 手動/自動
// TRUE:在WaitForSingleObject後必須手動調用ResetEvent清除信號
// FALSE:在WaitForSingleObject後,系統自動清除事件信號
BOOL bInitialState, //初始狀態
LPCTSTR lpName //事件的名稱
);
使用"事件"機制應注意如下事項:
(1)若是跨進程訪問事件,必須對事件命名,在對事件命名的時候,要注意不要與系統命名空間中的其它全局命名對象衝突;
(2)事件是否要自動恢復;
(3)事件的初始狀態設置。
因爲event對象屬於內核對象,故進程B能夠調用OpenEvent函數經過對象的名字得到進程A中event對象的句柄,而後將這個句柄用於ResetEvent、SetEvent和WaitForMultipleObjects等函數中。此法能夠實現一個進程的線程控制另外一進程中線程的運行,例如:
HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);
臨界區
定義臨界區變量
CRITICAL_SECTION gCriticalSection;
一般狀況下,CRITICAL_SECTION結構體應該被定義爲全局變量,以便於進程中的全部線程方便地按照變量名來引用該結構體。
初始化臨界區
VOID WINAPI InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向程序員定義的CRITICAL_SECTION變量
);
該函數用於對pcs所指的CRITICAL_SECTION結構體進行初始化。該函數只是設置了一些成員變量,它的運行通常不會失敗,所以它採用了VOID類型的返回值。該函數必須在任何線程調用EnterCriticalSection函數以前被調用,若是一個線程試圖進入一個未初始化的CRTICAL_SECTION,那麼結果將是很難預計的。
刪除臨界區
VOID WINAPI DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一個再也不須要的CRITICAL_SECTION變量
);
進入臨界區
VOID WINAPI EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一個你即將鎖定的CRITICAL_SECTION變量
);
離開臨界區
VOID WINAPI LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一個你即將離開的CRITICAL_SECTION變量
);
使用臨界區編程的通常方法是:
void UpdateData()
{
EnterCriticalSection(&gCriticalSection);
...//do something
LeaveCriticalSection(&gCriticalSection);
}
關於臨界區的使用,有下列注意點:
(1)每一個共享資源使用一個CRITICAL_SECTION變量;
(2)不要長時間運行關鍵代碼段,當一個關鍵代碼段長時間運行時,其餘線程就會進入等待狀態,這會下降應用程序的運行性能;
(3)若是須要同時訪問多個資源,則可能連續調用EnterCriticalSection;
(4)Critical Section不是OS核心對象,若是進入臨界區的線程"掛"了,將沒法釋放臨界資源。這個缺點在Mutex中獲得了彌補。
互斥
互斥量的做用是保證每次只能有一個線程得到互斥量而得以繼續執行,使用CreateMutex函數建立:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
// 安全屬性結構指針,可爲NULL
BOOL bInitialOwner,
//是否佔有該互斥量,TRUE:佔有,FALSE:不佔有
LPCTSTR lpName
//信號量的名稱
);
Mutex是核心對象,能夠跨進程訪問,下面的代碼給出了從另外一進程訪問命名Mutex的例子:
HANDLE hMutex;
hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"mutexName");
if (hMutex){
…
}
else{
…
}
相關API:
BOOL WINAPI ReleaseMutex(
HANDLE hMutex
);
使用互斥編程的通常方法是:
void UpdateResource()
{
WaitForSingleObject(hMutex,…);
...//do something
ReleaseMutex(hMutex);
}
互斥(mutex)內核對象可以確保線程擁有對單個資源的互斥訪問權。互斥對象的行爲特性與臨界區相同,可是互斥對象屬於內核對象,而臨界區則屬於用戶方式對象,所以這致使mutex與Critical Section的以下不一樣:
(1) 互斥對象的運行速度比關鍵代碼段要慢;
(2) 不一樣進程中的多個線程可以訪問單個互斥對象;
(3) 線程在等待訪問資源時能夠設定一個超時值。
下圖更詳細地列出了互斥與臨界區的不一樣:
臨界區
定義臨界區變量
CRITICAL_SECTION gCriticalSection;
一般狀況下,CRITICAL_SECTION結構體應該被定義爲全局變量,以便於進程中的全部線程方便地按照變量名來引用該結構體。
初始化臨界區
VOID WINAPI InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向程序員定義的CRITICAL_SECTION變量
);
該函數用於對pcs所指的CRITICAL_SECTION結構體進行初始化。該函數只是設置了一些成員變量,它的運行通常不會失敗,所以它採用了VOID類型的返回值。該函數必須在任何線程調用EnterCriticalSection函數以前被調用,若是一個線程試圖進入一個未初始化的CRTICAL_SECTION,那麼結果將是很難預計的。
刪除臨界區
VOID WINAPI DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一個再也不須要的CRITICAL_SECTION變量
);
進入臨界區
VOID WINAPI EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一個你即將鎖定的CRITICAL_SECTION變量
);
離開臨界區
VOID WINAPI LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
//指向一個你即將離開的CRITICAL_SECTION變量
);
使用臨界區編程的通常方法是:
void UpdateData()
{
EnterCriticalSection(&gCriticalSection);
...//do something
LeaveCriticalSection(&gCriticalSection);
}
關於臨界區的使用,有下列注意點:
(1)每一個共享資源使用一個CRITICAL_SECTION變量;
(2)不要長時間運行關鍵代碼段,當一個關鍵代碼段長時間運行時,其餘線程就會進入等待狀態,這會下降應用程序的運行性能;
(3)若是須要同時訪問多個資源,則可能連續調用EnterCriticalSection;
(4)Critical Section不是OS核心對象,若是進入臨界區的線程"掛"了,將沒法釋放臨界資源。這個缺點在Mutex中獲得了彌補。
互斥
互斥量的做用是保證每次只能有一個線程得到互斥量而得以繼續執行,使用CreateMutex函數建立:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
// 安全屬性結構指針,可爲NULL
BOOL bInitialOwner,
//是否佔有該互斥量,TRUE:佔有,FALSE:不佔有
LPCTSTR lpName
//信號量的名稱
);
Mutex是核心對象,能夠跨進程訪問,下面的代碼給出了從另外一進程訪問命名Mutex的例子:
HANDLE hMutex;
hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"mutexName");
if (hMutex){
…
}
else{
…
}
相關API:
BOOL WINAPI ReleaseMutex(
HANDLE hMutex
);
使用互斥編程的通常方法是:
void UpdateResource()
{
WaitForSingleObject(hMutex,…);
...//do something
ReleaseMutex(hMutex);
}
互斥(mutex)內核對象可以確保線程擁有對單個資源的互斥訪問權。互斥對象的行爲特性與臨界區相同,可是互斥對象屬於內核對象,而臨界區則屬於用戶方式對象,所以這致使mutex與Critical Section的以下不一樣:
(1) 互斥對象的運行速度比關鍵代碼段要慢;
(2) 不一樣進程中的多個線程可以訪問單個互斥對象;
(3) 線程在等待訪問資源時能夠設定一個超時值。