1. 程式人生 > >從單執行緒到多執行緒之執行緒通訊

從單執行緒到多執行緒之執行緒通訊

執行緒之間通訊的兩個基本問題是互斥和同步。

  執行緒同步是指執行緒之間所具有的一種制約關係,一個執行緒的執行依賴另一個執行緒的訊息,當它沒有得到另一個執行緒的訊息時應等待,直到訊息到達時才被喚醒。

執行緒互斥是指對於共享的作業系統資源(指的是廣義的"資源",而不是Windows的.res檔案,譬如全域性變數就是一種共享資源),在各執行緒訪問時的排它性。當有若干個執行緒都要使用某一共享資源時,任何時刻最多隻允許一個執行緒去使用,其它要使用該資源的執行緒必須等待,直到佔用資源者釋放該資源。

 在WIN32中,同步機制主要有以下幾種:

  (1)事件(Event);

  (2)訊號量(semaphore);

  (3)互斥量(mutex);

  (4)臨界區(Critical section)。

全域性變數

  因為程序中的所有執行緒均可以訪問所有的全域性變數,因而全域性變數成為Win32多執行緒通訊的最簡單方式。

事件

  事件(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變數
);

       推薦使用TryEnterCriticalSection因為如果EnterCriticalSection將一個執行緒置於等待狀態,那麼該執行緒在很長時間內就不能再次被排程。實際上,在編寫得不好的應用程式中,該執行緒永遠不會再次被賦予CPU時間。TryEnterCriticalSection函式決不允許呼叫執行緒進入等待狀態。它的返回值能夠指明呼叫執行緒是否能夠獲得對資源的訪問權。TryEnterCriticalSection發現該資源已經被另一個執行緒訪問,它就返回FALSE。在其他所有情況下,它均返回TRUE。運用這個函式,執行緒能夠迅速檢視它是否可以訪問某個共享資源,如果不能訪問,那麼它可以繼續執行某些其他操作,而不必進行等待。如果TryEnterCriticalSection函式確實返回了TRUE,那麼CRITICAL_SECTION的成員變數已經更新。


  離開臨界區

VOID WINAPI LeaveCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection
 //指向一個你即將離開的CRITICAL_SECTION變數
);

關於臨界區的使用,有下列注意點:

  (1)每個共享資源使用一個CRITICAL_SECTION變數;

  (2)不要長時間執行關鍵程式碼段,當一個關鍵程式碼段長時間執行時,其他執行緒就會進入等待狀態,這會降低應用程式的執行效能;

  (3)如果需要同時訪問多個資源,則可能連續呼叫EnterCriticalSection;

  (4)Critical Section不是OS核心物件,如果進入臨界區的執行緒"掛"了,將無法釋放臨界資源。這個缺點在Mutex中得到了彌補

互斥

  互斥量的作用是保證每次只能有一個執行緒獲得互斥量而得以繼續執行, Mutex是核心物件,可以跨程序訪問,使用CreateMutex函式建立:

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes,
 // 安全屬性結構指標,可為NULL
 BOOL bInitialOwner,
 //是否佔有該互斥量,TRUE:佔有,FALSE:不佔有
 LPCTSTR lpName
 //訊號量的名稱
);

互斥(mutex)核心物件能夠確保執行緒擁有對單個資源的互斥訪問權。互斥物件的行為特性與臨界區相同,但是互斥物件屬於核心物件,而臨界區則屬於使用者方式物件,因此這導致mutex與Critical Section的如下不同:

  (1) 互斥物件的執行速度比臨界區要慢;

  (2) 不同程序中的多個執行緒能夠訪問單個互斥物件;

  (3) 執行緒在等待訪問資源時可以設定一個超時值。

訊號量

  訊號量是維護0到指定最大值之間的同步物件。訊號量狀態在其計數大於0時是有訊號的,而其計數是0時是無訊號的。訊號量物件在控制上可以支援有限數量共享資源的訪問。

  訊號量的特點和用途可用下列幾句話定義:

  (1)如果當前資源的數量大於0,則訊號量有效;

  (2)如果當前資源數量是0,則訊號量無效;

  (3)系統決不允許當前資源的數量為負值;

  (4)當前資源數量決不能大於最大資源數量。

  建立訊號量

HANDLE CreateSemaphore (
 PSECURITY_ATTRIBUTE psa,
 LONG lInitialCount, //開始時可供使用的資源數
 LONG lMaximumCount, //最大資源數
PCTSTR pszName);


  釋放訊號量

  通過呼叫ReleaseSemaphore函式,執行緒就能夠對信標的當前資源數量進行遞增,該函式原型為:

BOOL WINAPI ReleaseSemaphore(
 HANDLE hSemaphore,
 LONG lReleaseCount, //訊號量的當前資源數增加lReleaseCount
 LPLONG lpPreviousCount
);


  開啟訊號量

  和其他核心物件一樣,訊號量也可以通過名字跨程序訪問,開啟訊號量的API為:

HANDLE OpenSemaphore (
 DWORD fdwAccess,
 BOOL bInherithandle,
 PCTSTR pszName
);


 互鎖訪問

  當必須以原子操作方式來修改單個值時,互鎖訪問函式是相當有用的。所謂原子訪問,是指執行緒在訪問資源時能夠確保所有其他執行緒都不在同一時間內訪問相同的資源。
互鎖訪問的控制速度非常快,呼叫一個互鎖函式的CPU週期通常小於50,不需要進行使用者方式與核心方式的切換(該切換通常需要執行1000個CPU週期)。

(1) LONG InterlockedExchangeAdd ( LPLONG Addend, LONG Increment );
Addend為長整型變數的地址,Increment為想要在Addend指向的長整型變數上增加的數值(可以是負數)。這個函式的主要作用是保證這個加操作為一個原子訪問。
(2) LONG InterlockedExchange( LPLONG Target, LONG Value );
用第二個引數的值取代第一個引數指向的值。函式返回值為原始值。
(3) PVOID InterlockedExchangePointer( PVOID *Target, PVOID Value );
用第二個引數的值取代第一個引數指向的值。函式返回值為原始值。
(4) LONG InterlockedCompareExchange(
LPLONG Destination, LONG Exchange, LONG Comperand  );
如果第三個引數與第一個引數指向的值相同,那麼用第二個引數取代第一個引數指向的值。函式返回值為原始值。
(5) PVOID InterlockedCompareExchangePointer (
PVOID *Destination, PVOID Exchange, PVOID Comperand );
如果第三個引數與第一個引數指向的值相同,那麼用第二個引數取代第一個引數指向的值。函式返回值為原始值。


  互鎖訪問函式的缺點在於其只能對單一變數進行原子訪問,如果要訪問的資源比較複雜,仍要使用臨界區或互斥。

可等待定時器

  可等待定時器物件在非啟用狀態下被建立,程式設計師應呼叫 SetWaitableTimer函式來界定定時器在何時被啟用:

BOOL SetWaitableTimer(
 HANDLE hTimer, //要設定的定時器
 const LARGE_INTEGER *pDueTime, //指明定時器第一次啟用的時間
 LONG lPeriod, //指明此後定時器應該間隔多長時間啟用一次
 PTIMERAPCROUTINE pfnCompletionRoutine,
 PVOID PvArgToCompletionRoutine,
BOOL fResume);

  取消可等待定時器

BOOl Cancel WaitableTimer(
 HANDLE hTimer //要取消的定時器
);

  開啟可等待定時器

  作為一種核心物件,WaitableTimer也可以被其他程序以名字開啟:

HANDLE OpenWaitableTimer (
 DWORD fdwAccess,
 BOOL bInherithandle,
 PCTSTR pszName
);