1. 程式人生 > >windows 多執行緒同步技術

windows 多執行緒同步技術

轉載自: 天極網,,

摘要: 多執行緒同步技術是計算機軟體開發的重要技術,本文對多執行緒的各種同步技術的原理和實現進行了初步探討。

  關鍵詞: VC++6.0; 執行緒同步;臨界區;事件;互斥;訊號量;

  閱讀目錄:

  使執行緒同步
  臨界區
  管理事件核心物件
  訊號量核心物件
  互斥核心物件
  小結

  正文

  使執行緒同步

  在程式中使用多執行緒時,一般很少有多個執行緒能在其生命期內進行完全獨立的操作。更多的情況是一些執行緒進行某些處理操作,而其他的執行緒必須對其處理結果進行了解。正常情況下對這種處理結果的瞭解應當在其處理任務完成後進行。

  如果不採取適當的措施,其他執行緒往往會線上程處理任務結束前就去訪問處理結果,這就很有可能得到有關處理結果的錯誤瞭解。例如,多個執行緒同時訪問同一個全域性變數,如果都是讀取操作,則不會出現問題。如果一個執行緒負責改變此變數的值,而其他執行緒負責同時讀取變數內容,則不能保證讀取到的資料是經過寫執行緒修改後的。

  為了確保讀執行緒讀取到的是經過修改的變數,就必須在向變數寫入資料時禁止其他執行緒對其的任何訪問,直至賦值過程結束後再解除對其他執行緒的訪問限制。象這種保證執行緒能瞭解其他執行緒任務處理結束後的處理結果而採取的保護措施即為執行緒同步。

  執行緒同步是一個非常大的話題,包括方方面面的內容。從大的方面講,執行緒的同步可分使用者模式的執行緒同步和核心物件的執行緒同步兩大類。使用者模式中執行緒的同步方法主要有原子訪問和臨界區等方法。其特點是同步速度特別快,適合於對執行緒執行速度有嚴格要求的場合。

  核心物件的執行緒同步則主要由事件、等待定時器、訊號量以及訊號燈等核心物件構成。由於這種同步機制使用了核心物件,使用時必須將執行緒從使用者模式切換到核心模式,而這種轉換一般要耗費近千個

CPU週期,因此同步速度較慢,但在適用性上卻要遠優於使用者模式的執行緒同步方式。

一、臨界區

  臨界區(Critical Section)是一段獨佔對某些共享資源訪問的程式碼,在任意時刻只允許一個執行緒對共享資源進行訪問。如果有多個執行緒試圖同時訪問臨界區,那麼在有一個執行緒進入後其他所有試圖訪問此臨界區的執行緒將被掛起,並一直持續到進入臨界區的執行緒離開。臨界區在被釋放後,其他執行緒可以繼續搶佔,並以此達到用原子方式操作共享資源的目的。

  臨界區在使用時以CRITICAL_SECTION結構物件保護共享資源,並分別用EnterCriticalSection()和LeaveCriticalSection()函式去標識和釋放一個臨界區。所用到的CRITICAL_SECTION結構物件必須經過InitializeCriticalSection()的初始化後才能使用,而且必須確保所有執行緒中的任何試圖訪問此共享資源的程式碼都處在此臨界區的保護之下。否則臨界區將不會起到應有的作用,共享資源依然有被破壞的可能。


圖1 使用臨界區保持執行緒同步

  下面通過一段程式碼展示了臨界區在保護多執行緒訪問的共享資源中的作用。通過兩個執行緒來分別對全域性變數g_cArray[10]進行寫入操作,用臨界區結構物件g_cs來保持執行緒的同步,並在開啟執行緒前對其進行初始化。為了使實驗效果更加明顯,體現出臨界區的作用,線上程函式對共享資源g_cArray[10]的寫入時,以Sleep()函式延遲1毫秒,使其他執行緒同其搶佔CPU的可能性增大。如果不使用臨界區對其進行保護,則共享資源資料將被破壞(參見圖1(a)所示計算結果),而使用臨界區對執行緒保持同步後則可以得到正確的結果(參見圖1(b)所示計算結果)。程式碼實現清單附下:

// 臨界區結構物件
CRITICAL_SECTION g_cs;
// 共享資源
char g_cArray[10];
UINT ThreadProc10(LPVOID pParam)
{
 // 進入臨界區
 EnterCriticalSection(&g_cs);
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 離開臨界區
 LeaveCriticalSection(&g_cs);
 return 0;
}
UINT ThreadProc11(LPVOID pParam)
{
 // 進入臨界區
 EnterCriticalSection(&g_cs);
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 離開臨界區
 LeaveCriticalSection(&g_cs);
 return 0;
}
……
void CSample08View::OnCriticalSection()
{
 // 初始化臨界區
 InitializeCriticalSection(&g_cs);
 // 啟動執行緒
 AfxBeginThread(ThreadProc10, NULL);
 AfxBeginThread(ThreadProc11, NULL);
 // 等待計算完畢
 Sleep(300);
 // 報告計算結果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

  在使用臨界區時,一般不允許其執行時間過長,只要進入臨界區的執行緒還沒有離開,其他所有試圖進入此臨界區的執行緒都會被掛起而進入到等待狀態,並會在一定程度上影響。程式的執行效能。尤其需要注意的是不要將等待使用者輸入或是其他一些外界干預的操作包含到臨界區。如果進入了臨界區卻一直沒有釋放,同樣也會引起其他執行緒的長時間等待。換句話說,在執行了EnterCriticalSection()語句進入臨界區後無論發生什麼,必須確保與之匹配的LeaveCriticalSection()都能夠被執行到。可以通過新增結構化異常處理程式碼來確保LeaveCriticalSection()語句的執行。雖然臨界區同步速度很快,但卻只能用來同步本程序內的執行緒,而不可用來同步多個程序中的執行緒。

  MFC為臨界區提供有一個CCriticalSection類,使用該類進行執行緒同步處理是非常簡單的,只需線上程函式中用CCriticalSection類成員函式Lock()和UnLock()標定出被保護程式碼片段即可。對於上述程式碼,可通過CCriticalSection類將其改寫如下:

// MFC臨界區類物件
CCriticalSection g_clsCriticalSection;
// 共享資源
char g_cArray[10];
UINT ThreadProc20(LPVOID pParam)
{
 // 進入臨界區
 g_clsCriticalSection.Lock();
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 離開臨界區
 g_clsCriticalSection.Unlock();
 return 0;
}
UINT ThreadProc21(LPVOID pParam)
{
 // 進入臨界區
 g_clsCriticalSection.Lock();
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 離開臨界區
 g_clsCriticalSection.Unlock();
 return 0;
}
……
void CSample08View::OnCriticalSectionMfc()
{
 // 啟動執行緒
 AfxBeginThread(ThreadProc20, NULL);
 AfxBeginThread(ThreadProc21, NULL);
 // 等待計算完畢
 Sleep(300);
 // 報告計算結果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

二、管理事件核心物件

  在前面講述執行緒通訊時曾使用過事件核心物件來進行執行緒間的通訊,除此之外,事件核心物件也可以通過通知操作的方式來保持執行緒的同步。對於前面那段使用臨界區保持執行緒同步的程式碼可用事件物件的執行緒同步方法改寫如下:

// 事件控制代碼
HANDLE hEvent = NULL;
// 共享資源
char g_cArray[10];
……
UINT ThreadProc12(LPVOID pParam)
{
 // 等待事件置位
 WaitForSingleObject(hEvent, INFINITE);
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 處理完成後即將事件物件置位
 SetEvent(hEvent);
 return 0;
}
UINT ThreadProc13(LPVOID pParam)
{
 // 等待事件置位
 WaitForSingleObject(hEvent, INFINITE);
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 處理完成後即將事件物件置位
 SetEvent(hEvent);
 return 0;
}
……
void CSample08View::OnEvent()
{
 // 建立事件
 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
 // 事件置位
 SetEvent(hEvent);
 // 啟動執行緒
 AfxBeginThread(ThreadProc12, NULL);
 AfxBeginThread(ThreadProc13, NULL);
 // 等待計算完畢
 Sleep(300);
 // 報告計算結果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

  在建立執行緒前,首先建立一個可以自動復位的事件核心物件hEvent,而執行緒函式則通過WaitForSingleObject()等待函式無限等待hEvent的置位,只有在事件置位時WaitForSingleObject()才會返回,被保護的程式碼將得以執行。對於以自動復位方式建立的事件物件,在其置位後一被WaitForSingleObject()等待到就會立即復位,也就是說在執行ThreadProc12()中的受保護程式碼時,事件物件已經是復位狀態的,這時即使有ThreadProc13()對CPU的搶佔,也會由於WaitForSingleObject()沒有hEvent的置位而不能繼續執行,也就沒有可能破壞受保護的共享資源。在ThreadProc12()中的處理完成後可以通過SetEvent()對hEvent的置位而允許ThreadProc13()對共享資源g_cArray的處理。這裡SetEvent()所起的作用可以看作是對某項特定任務完成的通知。

  使用臨界區只能同步同一程序中的執行緒,而使用事件核心物件則可以對程序外的執行緒進行同步,其前提是得到對此事件物件的訪問權。可以通過OpenEvent()函式獲取得到,其函式原型為:

HANDLE OpenEvent(
 DWORD dwDesiredAccess, // 訪問標誌
 BOOL bInheritHandle, // 繼承標誌
 LPCTSTR lpName // 指向事件物件名的指標
);

  如果事件物件已建立(在建立事件時需要指定事件名),函式將返回指定事件的控制代碼。對於那些在建立事件時沒有指定事件名的事件核心物件,可以通過使用核心物件的繼承性或是呼叫DuplicateHandle()函式來呼叫CreateEvent()以獲得對指定事件物件的訪問權。在獲取到訪問權後所進行的同步操作與在同一個程序中所進行的執行緒同步操作是一樣的。

  如果需要在一個執行緒中等待多個事件,則用WaitForMultipleObjects()來等待。WaitForMultipleObjects()與WaitForSingleObject()類似,同時監視位於控制代碼陣列中的所有控制代碼。這些被監視物件的控制代碼享有平等的優先權,任何一個控制代碼都不可能比其他控制代碼具有更高的優先權。WaitForMultipleObjects()的函式原型為:

DWORD WaitForMultipleObjects(
 DWORD nCount, // 等待控制代碼數
 CONST HANDLE *lpHandles, // 控制代碼陣列首地址
 BOOL fWaitAll, // 等待標誌
 DWORD dwMilliseconds // 等待時間間隔
);

  引數nCount指定了要等待的核心物件的數目,存放這些核心物件的陣列由lpHandles來指向。fWaitAll對指定的這nCount個核心物件的兩種等待方式進行了指定,為TRUE時當所有物件都被通知時函式才會返回,為FALSE則只要其中任何一個得到通知就可以返回。dwMilliseconds在這裡的作用與在WaitForSingleObject()中的作用是完全一致的。如果等待超時,函式將返回WAIT_TIMEOUT。如果返回WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1中的某個值,則說明所有指定物件的狀態均為已通知狀態(當fWaitAll為TRUE時)或是用以減去WAIT_OBJECT_0而得到發生通知的物件的索引(當fWaitAll為FALSE時)。如果返回值在WAIT_ABANDONED_0與WAIT_ABANDONED_0+nCount-1之間,則表示所有指定物件的狀態均為已通知,且其中至少有一個物件是被丟棄的互斥物件(當fWaitAll為TRUE時),或是用以減去WAIT_OBJECT_0表示一個等待正常結束的互斥物件的索引(當fWaitAll為FALSE時)。 下面給出的程式碼主要展示了對WaitForMultipleObjects()函式的使用。通過對兩個事件核心物件的等待來控制執行緒任務的執行與中途退出:

// 存放事件控制代碼的陣列
HANDLE hEvents[2];
UINT ThreadProc14(LPVOID pParam)
{
 // 等待開啟事件
 DWORD dwRet1 = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE);
 // 如果開啟事件到達則執行緒開始執行任務
 if (dwRet1 == WAIT_OBJECT_0)
 {
  AfxMessageBox("執行緒開始工作!");
  while (true)
  {
   for (int i = 0; i < 10000; i++);
   // 在任務處理過程中等待結束事件
   DWORD dwRet2 = WaitForMultipleObjects(2, hEvents, FALSE, 0);
   // 如果結束事件置位則立即終止任務的執行
   if (dwRet2 == WAIT_OBJECT_0 + 1)
    break;
  }
 }
 AfxMessageBox("執行緒退出!");
 return 0;
}
……
void CSample08View::OnStartEvent()
{
 // 建立執行緒
 for (int i = 0; i < 2; i++)
  hEvents[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
  // 開啟執行緒
  AfxBeginThread(ThreadProc14, NULL);
  // 設定事件0(開啟事件)
  SetEvent(hEvents[0]);
}
void CSample08View::OnEndevent()
{
 // 設定事件1(結束事件)
 SetEvent(hEvents[1]);
}

  MFC為事件相關處理也提供了一個CEvent類,共包含有除建構函式外的4個成員函式PulseEvent()、ResetEvent()、SetEvent()和UnLock()。在功能上分別相當與Win32 API的PulseEvent()、ResetEvent()、SetEvent()和CloseHandle()等函式。而建構函式則履行了原CreateEvent()函式建立事件物件的職責,其函式原型為:

CEvent(BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );

  按照此預設設定將建立一個自動復位、初始狀態為復位狀態的沒有名字的事件物件。封裝後的CEvent類使用起來更加方便,圖2即展示了CEvent類對A、B兩執行緒的同步過程:


圖2 CEvent類對執行緒的同步過程示意

  B執行緒在執行到CEvent類成員函式Lock()時將會發生阻塞,而A執行緒此時則可以在沒有B執行緒干擾的情況下對共享資源進行處理,並在處理完成後通過成員函式SetEvent()向B發出事件,使其被釋放,得以對A先前已處理完畢的共享資源進行操作。可見,使用CEvent類對執行緒的同步方法與通過API函式進行執行緒同步的處理方法是基本一致的。前面的API處理程式碼可用CEvent類將其改寫為:

// MFC事件類物件
CEvent g_clsEvent;
UINT ThreadProc22(LPVOID pParam)
{
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 事件置位
 g_clsEvent.SetEvent();
 return 0;
}
UINT ThreadProc23(LPVOID pParam)
{
 // 等待事件
 g_clsEvent.Lock();
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 return 0;
}
……
void CSample08View::OnEventMfc()
{
 // 啟動執行緒
 AfxBeginThread(ThreadProc22, NULL);
 AfxBeginThread(ThreadProc23, NULL);
 // 等待計算完畢
 Sleep(300);
 // 報告計算結果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

三、訊號量核心物件

  訊號量(Semaphore)核心物件對執行緒的同步方式與前面幾種方法不同,它允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目。在用CreateSemaphore()建立訊號量時即要同時指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數設定為最大資源計數,每增加一個執行緒對共享資源的訪問,當前可用資源計數就會減1,只要當前可用資源計數是大於0的,就可以發出訊號量訊號。但是當前可用計數減小到0時則說明當前佔用資源的執行緒數已經達到了所允許的最大數目,不能在允許其他執行緒的進入,此時的訊號量訊號將無法發出。執行緒在處理完共享資源後,應在離開的同時通過ReleaseSemaphore()函式將當前可用資源計數加1。在任何時候當前可用資源計數決不可能大於最大資源計數。


圖3 使用訊號量物件控制資源

  下面結合圖例3來演示訊號量物件對資源的控制。在圖3中,以箭頭和白色箭頭表示共享資源所允許的最大資源計數和當前可用資源計數。初始如圖(a)所示,最大資源計數和當前可用資源計數均為4,此後每增加一個對資源進行訪問的執行緒(用黑色箭頭表示)當前資源計數就會相應減1,圖(b)即表示的在3個執行緒對共享資源進行訪問時的狀態。當進入執行緒數達到4個時,將如圖(c)所示,此時已達到最大資源計數,而當前可用資源計數也已減到0,其他執行緒無法對共享資源進行訪問。在當前佔有資源的執行緒處理完畢而退出後,將會釋放出空間,圖(d)已有兩個執行緒退出對資源的佔有,當前可用計數為2,可以再允許2個執行緒進入到對資源的處理。可以看出,訊號量是通過計數來對執行緒訪問資源進行控制的,而實際上訊號量確實也被稱作Dijkstra計數器。

  使用訊號量核心物件進行執行緒同步主要會用到CreateSemaphore()、OpenSemaphore()、ReleaseSemaphore()、WaitForSingleObject()和WaitForMultipleObjects()等函式。其中,CreateSemaphore()用來建立一個訊號量核心物件,其函式原型為:

HANDLE CreateSemaphore(
 LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全屬性指標
 LONG lInitialCount, // 初始計數
 LONG lMaximumCount, // 最大計數
 LPCTSTR lpName // 物件名指標
);

  引數lMaximumCount是一個有符號32位值,定義了允許的最大資源計數,最大取值不能超過4294967295。lpName引數可以為建立的訊號量定義一個名字,由於其建立的是一個核心物件,因此在其他程序中可以通過該名字而得到此訊號量。OpenSemaphore()函式即可用來根據訊號量名開啟在其他程序中建立的訊號量,函式原型如下:

HANDLE OpenSemaphore(
 DWORD dwDesiredAccess, // 訪問標誌
 BOOL bInheritHandle, // 繼承標誌
 LPCTSTR lpName // 訊號量名
);

  線上程離開對共享資源的處理時,必須通過ReleaseSemaphore()來增加當前可用資源計數。否則將會出現當前正在處理共享資源的實際執行緒數並沒有達到要限制的數值,而其他執行緒卻因為當前可用資源計數為0而仍無法進入的情況。ReleaseSemaphore()的函式原型為:

BOOL ReleaseSemaphore(
 HANDLE hSemaphore, // 訊號量控制代碼
 LONG lReleaseCount, // 計數遞增數量
 LPLONG lpPreviousCount // 先前計數
);

  該函式將lReleaseCount中的值新增給訊號量的當前資源計數,一般將lReleaseCount設定為1,如果需要也可以設定其他的值。WaitForSingleObject()和WaitForMultipleObjects()主要用在試圖進入共享資源的執行緒函式入口處,主要用來判斷訊號量的當前可用資源計數是否允許本執行緒的進入。只有在當前可用資源計數值大於0時,被監視的訊號量核心物件才會得到通知。

  訊號量的使用特點使其更適用於對Socket(套接字)程式中執行緒的同步。例如,網路上的HTTP伺服器要對同一時間內訪問同一頁面的使用者數加以限制,這時可以為沒一個使用者對伺服器的頁面請求設定一個執行緒,而頁面則是待保護的共享資源,通過使用訊號量對執行緒的同步作用可以確保在任一時刻無論有多少使用者對某一頁面進行訪問,只有不大於設定的最大使用者數目的執行緒能夠進行訪問,而其他的訪問企圖則被掛起,只有在有使用者退出對此頁面的訪問後才有可能進入。下面給出的示例程式碼即展示了類似的處理過程:

// 訊號量物件控制代碼
HANDLE hSemaphore;
UINT ThreadProc15(LPVOID pParam)
{
 // 試圖進入訊號量關口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 執行緒任務處理
 AfxMessageBox("執行緒一正在執行!");
 // 釋放訊號量計數
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
UINT ThreadProc16(LPVOID pParam)
{
 // 試圖進入訊號量關口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 執行緒任務處理
 AfxMessageBox("執行緒二正在執行!");
 // 釋放訊號量計數
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
UINT ThreadProc17(LPVOID pParam)
{
 // 試圖進入訊號量關口
 WaitForSingleObject(hSemaphore, INFINITE);
 // 執行緒任務處理
 AfxMessageBox("執行緒三正在執行!");
 // 釋放訊號量計數
 ReleaseSemaphore(hSemaphore, 1, NULL);
 return 0;
}
……
void CSample08View::OnSemaphore()
{
 // 建立訊號量物件
 hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
 // 開啟執行緒
 AfxBeginThread(ThreadProc15, NULL);
 AfxBeginThread(ThreadProc16, NULL);
 AfxBeginThread(ThreadProc17, NULL);
}


圖4 開始進入的兩個執行緒


圖5 執行緒二退出後執行緒三才得以進入

  上述程式碼在開啟執行緒前首先建立了一個初始計數和最大資源計數均為2的訊號量物件hSemaphore。即在同一時刻只允許2個執行緒進入由hSemaphore保護的共享資源。隨後開啟的三個執行緒均試圖訪問此共享資源,在前兩個執行緒試圖訪問共享資源時,由於hSemaphore的當前可用資源計數分別為2和1,此時的hSemaphore是可以得到通知的,也就是說位於執行緒入口處的WaitForSingleObject()將立即返回,而在前兩個執行緒進入到保護區域後,hSemaphore的當前資源計數減少到0,hSemaphore將不再得到通知,WaitForSingleObject()將執行緒掛起。直到此前進入到保護區的執行緒退出後才能得以進入。圖4和圖5為上述代脈的執行結果。從實驗結果可以看出,訊號量始終保持了同一時刻不超過2個執行緒的進入。

  在MFC中,通過CSemaphore類對訊號量作了表述。該類只具有一個建構函式,可以構造一個訊號量物件,並對初始資源計數、最大資源計數、物件名和安全屬性等進行初始化,其原型如下:

CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );

  在構造了CSemaphore類物件後,任何一個訪問受保護共享資源的執行緒都必須通過CSemaphore從父類CSyncObject類繼承得到的Lock()和UnLock()成員函式來訪問或釋放CSemaphore物件。與前面介紹的幾種通過MFC類保持執行緒同步的方法類似,通過CSemaphore類也可以將前面的執行緒同步程式碼進行改寫,這兩種使用訊號量的執行緒同步方法無論是在實現原理上還是從實現結果上都是完全一致的。下面給出經MFC改寫後的訊號量執行緒同步程式碼:

// MFC訊號量類物件
CSemaphore g_clsSemaphore(2, 2);
UINT ThreadProc24(LPVOID pParam)
{
 // 試圖進入訊號量關口
 g_clsSemaphore.Lock();
 // 執行緒任務處理
 AfxMessageBox("執行緒一正在執行!");
 // 釋放訊號量計數
 g_clsSemaphore.Unlock();
 return 0;
}
UINT ThreadProc25(LPVOID pParam)
{
 // 試圖進入訊號量關口
 g_clsSemaphore.Lock();
 // 執行緒任務處理
 AfxMessageBox("執行緒二正在執行!");
 // 釋放訊號量計數
 g_clsSemaphore.Unlock();
 return 0;
}
UINT ThreadProc26(LPVOID pParam)
{
 // 試圖進入訊號量關口
 g_clsSemaphore.Lock();
 // 執行緒任務處理
 AfxMessageBox("執行緒三正在執行!");
 // 釋放訊號量計數
 g_clsSemaphore.Unlock();
 return 0;
}
……
void CSample08View::OnSemaphoreMfc()
{
 // 開啟執行緒
 AfxBeginThread(ThreadProc24, NULL);
 AfxBeginThread(ThreadProc25, NULL);
 AfxBeginThread(ThreadProc26, NULL);
}

四、互斥核心物件

  互斥(Mutex)是一種用途非常廣泛的核心物件。能夠保證多個執行緒對同一共享資源的互斥訪問。同臨界區有些類似,只有擁有互斥物件的執行緒才具有訪問資源的許可權,由於互斥物件只有一個,因此就決定了任何情況下此共享資源都不會同時被多個執行緒所訪問。當前佔據資源的執行緒在任務處理完後應將擁有的互斥物件交出,以便其他執行緒在獲得後得以訪問資源。與其他幾種核心物件不同,互斥物件在作業系統中擁有特殊程式碼,並由作業系統來管理,作業系統甚至還允許其進行一些其他核心物件所不能進行的非常規操作。為便於理解,可參照圖6給出的互斥核心物件的工作模型:


圖6 使用互斥核心物件對共享資源的保護

  圖(a)中的箭頭為要訪問資源(矩形框)的執行緒,但只有第二個執行緒擁有互斥物件(黑點)並得以進入到共享資源,而其他執行緒則會被排斥在外(如圖(b)所示)。當此執行緒處理完共享資源並準備離開此區域時將把其所擁有的互斥物件交出(如圖(c)所示),其他任何一個試圖訪問此資源的執行緒都有機會得到此互斥物件。

  以互斥核心物件來保持執行緒同步可能用到的函式主要有CreateMutex()、OpenMutex()、ReleaseMutex()、WaitForSingleObject()和WaitForMultipleObjects()等。在使用互斥物件前,首先要通過CreateMutex()或OpenMutex()建立或開啟一個互斥物件。CreateMutex()函式原型為:

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全屬性指標
 BOOL bInitialOwner, // 初始擁有者
 LPCTSTR lpName // 互斥物件名
);

  引數bInitialOwner主要用來控制互斥物件的初始狀態。一般多將其設定為FALSE,以表明互斥物件在建立時並沒有為任何執行緒所佔有。如果在建立互斥物件時指定了物件名,那麼可以在本程序其他地方或是在其他程序通過OpenMutex()函式得到此互斥物件的控制代碼。OpenMutex()函式原型為:

HANDLE OpenMutex(
 DWORD dwDesiredAccess, // 訪問標誌
 BOOL bInheritHandle, // 繼承標誌
 LPCTSTR lpName // 互斥物件名
);

  當目前對資源具有訪問權的執行緒不再需要訪問此資源而要離開時,必須通過ReleaseMutex()函式來釋放其擁有的互斥物件,其函式原型為:

BOOL ReleaseMutex(HANDLE hMutex);

  其唯一的引數hMutex為待釋放的互斥物件控制代碼。至於WaitForSingleObject()和WaitForMultipleObjects()等待函式在互斥物件保持執行緒同步中所起的作用與在其他核心物件中的作用是基本一致的,也是等待互斥核心物件的通知。但是這裡需要特別指出的是:在互斥物件通知引起呼叫等待函式返回時,等待函式的返回值不再是通常的WAIT_OBJECT_0(對於WaitForSingleObject()函式)或是在WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1之間的一個值(對於WaitForMultipleObjects()函式),而是將返回一個WAIT_ABANDONED_0(對於WaitForSingleObject()函式)或是在WAIT_ABANDONED_0到WAIT_ABANDONED_0+nCount-1之間的一個值(對於WaitForMultipleObjects()函式)。以此來表明執行緒正在等待的互斥物件由另外一個執行緒所擁有,而此執行緒卻在使用完共享資源前就已經終止。除此之外,使用互斥物件的方法在等待執行緒的可排程性上同使用其他幾種核心物件的方法也有所不同,其他核心物件在沒有得到通知時,受呼叫等待函式的作用,執行緒將會掛起,同時失去可排程性,而使用互斥的方法卻可以在等待的同時仍具有可排程性,這也正是互斥物件所能完成的非常規操作之一。

  在編寫程式時,互斥物件多用在對那些為多個執行緒所訪問的記憶體塊的保護上,可以確保任何執行緒在處理此記憶體塊時都對其擁有可靠的獨佔訪問權。下面給出的示例程式碼即通過互斥核心物件hMutex對共享記憶體快g_cArray[]進行執行緒的獨佔訪問保護。下面給出實現程式碼清單:

// 互斥物件
HANDLE hMutex = NULL;
char g_cArray[10];
UINT ThreadProc18(LPVOID pParam)
{
 // 等待互斥物件通知
 WaitForSingleObject(hMutex, INFINITE);
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 釋放互斥物件
 ReleaseMutex(hMutex);
 return 0;
}
UINT ThreadProc19(LPVOID pParam)
{
 // 等待互斥物件通知
 WaitForSingleObject(hMutex, INFINITE);
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 釋放互斥物件
 ReleaseMutex(hMutex);
 return 0;
}
……
void CSample08View::OnMutex()
{
 // 建立互斥物件
 hMutex = CreateMutex(NULL, FALSE, NULL);
 // 啟動執行緒
 AfxBeginThread(ThreadProc18, NULL);
 AfxBeginThread(ThreadProc19, NULL);
 // 等待計算完畢
 Sleep(300);
 // 報告計算結果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

  互斥物件在MFC中通過CMutex類進行表述。使用CMutex類的方法非常簡單,在構造CMutex類物件的同時可以指明待查詢的互斥物件的名字,在建構函式返回後即可訪問此互斥變數。CMutex類也是隻含有建構函式這唯一的成員函式,當完成對互斥物件保護資源的訪問後,可通過呼叫從父類CSyncObject繼承的UnLock()函式完成對互斥物件的釋放。CMutex類建構函式原型為:

CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );

  該類的適用範圍和實現原理與API方式建立的互斥核心物件是完全類似的,但要簡潔的多,下面給出就是對前面的示例程式碼經CMutex類改寫後的程式實現清單:

// MFC互斥類物件
CMutex g_clsMutex(FALSE, NULL);
UINT ThreadProc27(LPVOID pParam)
{
 // 等待互斥物件通知
 g_clsMutex.Lock();
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[i] = 'a';
  Sleep(1);
 }
 // 釋放互斥物件
 g_clsMutex.Unlock();
 return 0;
}
UINT ThreadProc28(LPVOID pParam)
{
 // 等待互斥物件通知
 g_clsMutex.Lock();
 // 對共享資源進行寫入操作
 for (int i = 0; i < 10; i++)
 {
  g_cArray[10 - i - 1] = 'b';
  Sleep(1);
 }
 // 釋放互斥物件
 g_clsMutex.Unlock();
 return 0;
}
……
void CSample08View::OnMutexMfc()
{
 // 啟動執行緒
 AfxBeginThread(ThreadProc27, NULL);
 AfxBeginThread(ThreadProc28, NULL);
 // 等待計算完畢
 Sleep(300);
 // 報告計算結果
 CString sResult = CString(g_cArray);
 AfxMessageBox(sResult);
}

  小結

  執行緒的使用使程式處理更夠更加靈活,而這種靈活同樣也會帶來各種不確定性的可能。尤其是在多個執行緒對同一公共變數進行訪問時。雖然未使用執行緒同步的程式程式碼在邏輯上或許沒有什麼問題,但為了確保程式的正確、可靠執行,必須在適當的場合採取執行緒同步措施。