中國電波傳播研究所 郎銳 yesky
摘要: 多線(xiàn)程同步技術(shù)是計算機軟件開(kāi)發(fā)的重要技術(shù),本文對多線(xiàn)程的各種同步技術(shù)的原理和實(shí)現進(jìn)行了初步探討。
關(guān)鍵詞: VC++6.0; 線(xiàn)程同步;臨界區;事件;互斥;信號量; 正文
使線(xiàn)程同步
在程序中使用多線(xiàn)程時(shí),一般很少有多個(gè)線(xiàn)程能在其生命期內進(jìn)行完全獨立的操作。更多的情況是一些線(xiàn)程進(jìn)行某些處理操作,而其他的線(xiàn)程必須對其處理結果進(jìn)行了解。正常情況下對這種處理結果的了解應當在其處理任務(wù)完成后進(jìn)行。
如果不采取適當的措施,其他線(xiàn)程往往會(huì )在線(xiàn)程處理任務(wù)結束前就去訪(fǎng)問(wèn)處理結果,這就很有可能得到有關(guān)處理結果的錯誤了解。例如,多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)同一個(gè)全局變量,如果都是讀取操作,則不會(huì )出現問(wèn)題。如果一個(gè)線(xiàn)程負責改變此變量的值,而其他線(xiàn)程負責同時(shí)讀取變量?jì)热?,則不能保證讀取到的數據是經(jīng)過(guò)寫(xiě)線(xiàn)程修改后的。
為了確保讀線(xiàn)程讀取到的是經(jīng)過(guò)修改的變量,就必須在向變量寫(xiě)入數據時(shí)禁止其他線(xiàn)程對其的任何訪(fǎng)問(wèn),直至賦值過(guò)程結束后再解除對其他線(xiàn)程的訪(fǎng)問(wèn)限制。象這種保證線(xiàn)程能了解其他線(xiàn)程任務(wù)處理結束后的處理結果而采取的保護措施即為線(xiàn)程同步。
線(xiàn)程同步是一個(gè)非常大的話(huà)題,包括方方面面的內容。從大的方面講,線(xiàn)程的同步可分用戶(hù)模式的線(xiàn)程同步和內核對象的線(xiàn)程同步兩大類(lèi)。用戶(hù)模式中線(xiàn)程的同步方法主要有原子訪(fǎng)問(wèn)和臨界區等方法。其特點(diǎn)是同步速度特別快,適合于對線(xiàn)程運行速度有嚴格要求的場(chǎng)合。
內核對象的線(xiàn)程同步則主要由事件、等待定時(shí)器、信號量以及信號燈等內核對象構成。由于這種同步機制使用了內核對象,使用時(shí)必須將線(xiàn)程從用戶(hù)模式切換到內核模式,而這種轉換一般要耗費近千個(gè)CPU周期,因此同步速度較慢,但在適用性上卻要遠優(yōu)于用戶(hù)模式的線(xiàn)程同步方式。
臨界區
臨界區(Critical Section)是一段獨占對某些共享資源訪(fǎng)問(wèn)的代碼,在任意時(shí)刻只允許一個(gè)線(xiàn)程對共享資源進(jìn)行訪(fǎng)問(wèn)。如果有多個(gè)線(xiàn)程試圖同時(shí)訪(fǎng)問(wèn)臨界區,那么在有一個(gè)線(xiàn)程進(jìn)入后其他所有試圖訪(fǎng)問(wèn)此臨界區的線(xiàn)程將被掛起,并一直持續到進(jìn)入臨界區的線(xiàn)程離開(kāi)。臨界區在被釋放后,其他線(xiàn)程可以繼續搶占,并以此達到用原子方式操作共享資源的目的。
臨界區在使用時(shí)以CRITICAL_SECTION結構對象保護共享資源,并分別用EnterCriticalSection()和LeaveCriticalSection()函數去標識和釋放一個(gè)臨界區。所用到的CRITICAL_SECTION結構對象必須經(jīng)過(guò)InitializeCriticalSection()的初始化后才能使用,而且必須確保所有線(xiàn)程中的任何試圖訪(fǎng)問(wèn)此共享資源的代碼都處在此臨界區的保護之下。否則臨界區將不會(huì )起到應有的作用,共享資源依然有被破壞的可能。
圖1 使用臨界區保持線(xiàn)程同步
下面通過(guò)一段代碼展示了臨界區在保護多線(xiàn)程訪(fǎng)問(wèn)的共享資源中的作用。通過(guò)兩個(gè)線(xiàn)程來(lái)分別對全局變量g_cArray[10]進(jìn)行寫(xiě)入操作,用臨界區結構對象g_cs來(lái)保持線(xiàn)程的同步,并在開(kāi)啟線(xiàn)程前對其進(jìn)行初始化。為了使實(shí)驗效果更加明顯,體現出臨界區的作用,在線(xiàn)程函數對共享資源g_cArray[10]的寫(xiě)入時(shí),以Sleep()函數延遲1毫秒,使其他線(xiàn)程同其搶占CPU的可能性增大。如果不使用臨界區對其進(jìn)行保護,則共享資源數據將被破壞(參見(jiàn)圖1(a)所示計算結果),而使用臨界區對線(xiàn)程保持同步后則可以得到正確的結果(參見(jiàn)圖1(b)所示計算結果)。代碼實(shí)現清單附下:
// 臨界區結構對象
CRITICAL_SECTION g_cs;
// 共享資源
char g_cArray[10];
UINT ThreadProc10(LPVOID pParam)
{
// 進(jìn)入臨界區
EnterCriticalSection(&g_cs);
// 對共享資源進(jìn)行寫(xiě)入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = ‘a(chǎn)‘;
Sleep(1);
}
// 離開(kāi)臨界區
LeaveCriticalSection(&g_cs);
return 0;
}
UINT ThreadProc11(LPVOID pParam)
{
// 進(jìn)入臨界區
EnterCriticalSection(&g_cs);
// 對共享資源進(jìn)行寫(xiě)入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = ‘b‘;
Sleep(1);
}
// 離開(kāi)臨界區
LeaveCriticalSection(&g_cs);
return 0;
}
……
void CSample08View::OnCriticalSection()
{
// 初始化臨界區
InitializeCriticalSection(&g_cs);
// 啟動(dòng)線(xiàn)程
AfxBeginThread(ThreadProc10, NULL);
AfxBeginThread(ThreadProc11, NULL);
// 等待計算完畢
Sleep(300);
// 報告計算結果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
在使用臨界區時(shí),一般不允許其運行時(shí)間過(guò)長(cháng),只要進(jìn)入臨界區的線(xiàn)程還沒(méi)有離開(kāi),其他所有試圖進(jìn)入此臨界區的線(xiàn)程都會(huì )被掛起而進(jìn)入到等待狀態(tài),并會(huì )在一定程度上影響。程序的運行性能。尤其需要注意的是不要將等待用戶(hù)輸入或是其他一些外界干預的操作包含到臨界區。如果進(jìn)入了臨界區卻一直沒(méi)有釋放,同樣也會(huì )引起其他線(xiàn)程的長(cháng)時(shí)間等待。換句話(huà)說(shuō),在執行了EnterCriticalSection()語(yǔ)句進(jìn)入臨界區后無(wú)論發(fā)生什么,必須確保與之匹配的LeaveCriticalSection()都能夠被執行到??梢酝ㄟ^(guò)添加結構化異常處理代碼來(lái)確保LeaveCriticalSection()語(yǔ)句的執行。雖然臨界區同步速度很快,但卻只能用來(lái)同步本進(jìn)程內的線(xiàn)程,而不可用來(lái)同步多個(gè)進(jìn)程中的線(xiàn)程。
MFC為臨界區提供有一個(gè)CCriticalSection類(lèi),使用該類(lèi)進(jìn)行線(xiàn)程同步處理是非常簡(jiǎn)單的,只需在線(xiàn)程函數中用CCriticalSection類(lèi)成員函數Lock()和UnLock()標定出被保護代碼片段即可。對于上述代碼,可通過(guò)CCriticalSection類(lèi)將其改寫(xiě)如下:
// MFC臨界區類(lèi)對象
CCriticalSection g_clsCriticalSection;
// 共享資源
char g_cArray[10];
UINT ThreadProc20(LPVOID pParam)
{
// 進(jìn)入臨界區
g_clsCriticalSection.Lock();
// 對共享資源進(jìn)行寫(xiě)入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = ‘a(chǎn)‘;
Sleep(1);
}
// 離開(kāi)臨界區
g_clsCriticalSection.Unlock();
return 0;
}
UINT ThreadProc21(LPVOID pParam)
{
// 進(jìn)入臨界區
g_clsCriticalSection.Lock();
// 對共享資源進(jìn)行寫(xiě)入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = ‘b‘;
Sleep(1);
}
// 離開(kāi)臨界區
g_clsCriticalSection.Unlock();
return 0;
}
……
void CSample08View::OnCriticalSectionMfc()
{
// 啟動(dòng)線(xiàn)程
AfxBeginThread(ThreadProc20, NULL);
AfxBeginThread(ThreadProc21, NULL);
// 等待計算完畢
Sleep(300);
// 報告計算結果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
管理事件內核對象
在前面講述線(xiàn)程通信時(shí)曾使用過(guò)事件內核對象來(lái)進(jìn)行線(xiàn)程間的通信,除此之外,事件內核對象也可以通過(guò)通知操作的方式來(lái)保持線(xiàn)程的同步。對于前面那段使用臨界區保持線(xiàn)程同步的代碼可用事件對象的線(xiàn)程同步方法改寫(xiě)如下:
// 事件句柄
HANDLE hEvent = NULL;
// 共享資源
char g_cArray[10];
……
UINT ThreadProc12(LPVOID pParam)
{
// 等待事件置位
WaitForSingleObject(hEvent, INFINITE);
// 對共享資源進(jìn)行寫(xiě)入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = ‘a(chǎn)‘;
Sleep(1);
}
// 處理完成后即將事件對象置位
SetEvent(hEvent);
return 0;
}
UINT ThreadProc13(LPVOID pParam)
{
// 等待事件置位
WaitForSingleObject(hEvent, INFINITE);
// 對共享資源進(jìn)行寫(xiě)入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = ‘b‘;
Sleep(1);
}
// 處理完成后即將事件對象置位
SetEvent(hEvent);
return 0;
}
……
void CSample08View::OnEvent()
{
// 創(chuàng )建事件
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 事件置位
SetEvent(hEvent);
// 啟動(dòng)線(xiàn)程
AfxBeginThread(ThreadProc12, NULL);
AfxBeginThread(ThreadProc13, NULL);
// 等待計算完畢
Sleep(300);
// 報告計算結果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
在創(chuàng )建線(xiàn)程前,首先創(chuàng )建一個(gè)可以自動(dòng)復位的事件內核對象hEvent,而線(xiàn)程函數則通過(guò)WaitForSingleObject()等待函數無(wú)限等待hEvent的置位,只有在事件置位時(shí)WaitForSingleObject()才會(huì )返回,被保護的代碼將得以執行。對于以自動(dòng)復位方式創(chuàng )建的事件對象,在其置位后一被WaitForSingleObject()等待到就會(huì )立即復位,也就是說(shuō)在執行ThreadProc12()中的受保護代碼時(shí),事件對象已經(jīng)是復位狀態(tài)的,這時(shí)即使有ThreadProc13()對CPU的搶占,也會(huì )由于WaitForSingleObject()沒(méi)有hEvent的置位而不能繼續執行,也就沒(méi)有可能破壞受保護的共享資源。在ThreadProc12()中的處理完成后可以通過(guò)SetEvent()對hEvent的置位而允許ThreadProc13()對共享資源g_cArray的處理。這里SetEvent()所起的作用可以看作是對某項特定任務(wù)完成的通知。
使用臨界區只能同步同一進(jìn)程中的線(xiàn)程,而使用事件內核對象則可以對進(jìn)程外的線(xiàn)程進(jìn)行同步,其前提是得到對此事件對象的訪(fǎng)問(wèn)權??梢酝ㄟ^(guò)OpenEvent()函數獲取得到,其函數原型為:
HANDLE OpenEvent(
DWORD dwDesiredAccess, // 訪(fǎng)問(wèn)標志
BOOL bInheritHandle, // 繼承標志
LPCTSTR lpName // 指向事件對象名的指針
);
如果事件對象已創(chuàng )建(在創(chuàng )建事件時(shí)需要指定事件名),函數將返回指定事件的句柄。對于那些在創(chuàng )建事件時(shí)沒(méi)有指定事件名的事件內核對象,可以通過(guò)使用內核對象的繼承性或是調用DuplicateHandle()函數來(lái)調用CreateEvent()以獲得對指定事件對象的訪(fǎng)問(wèn)權。在獲取到訪(fǎng)問(wèn)權后所進(jìn)行的同步操作與在同一個(gè)進(jìn)程中所進(jìn)行的線(xiàn)程同步操作是一樣的。
如果需要在一個(gè)線(xiàn)程中等待多個(gè)事件,則用WaitForMultipleObjects()來(lái)等待。WaitForMultipleObjects()與WaitForSingleObject()類(lèi)似,同時(shí)監視位于句柄數組中的所有句柄。這些被監視對象的句柄享有平等的優(yōu)先權,任何一個(gè)句柄都不可能比其他句柄具有更高的優(yōu)先權。WaitForMultipleObjects()的函數原型為:
DWORD WaitForMultipleObjects(
DWORD nCount, // 等待句柄數
CONST HANDLE *lpHandles, // 句柄數組首地址
BOOL fWaitAll, // 等待標志
DWORD dwMilliseconds // 等待時(shí)間間隔
);
參數nCount指定了要等待的內核對象的數目,存放這些內核對象的數組由lpHandles來(lái)指向。fWaitAll對指定的這nCount個(gè)內核對象的兩種等待方式進(jìn)行了指定,為T(mén)RUE時(shí)當所有對象都被通知時(shí)函數才會(huì )返回,為FALSE則只要其中任何一個(gè)得到通知就可以返回。dwMilliseconds在這里的作用與在WaitForSingleObject()中的作用是完全一致的。如果等待超時(shí),函數將返回WAIT_TIMEOUT。如果返回WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1中的某個(gè)值,則說(shuō)明所有指定對象的狀態(tài)均為已通知狀態(tài)(當fWaitAll為T(mén)RUE時(shí))或是用以減去WAIT_OBJECT_0而得到發(fā)生通知的對象的索引(當fWaitAll為FALSE時(shí))。如果返回值在WAIT_ABANDONED_0與WAIT_ABANDONED_0+nCount-1之間,則表示所有指定對象的狀態(tài)均為已通知,且其中至少有一個(gè)對象是被丟棄的互斥對象(當fWaitAll為T(mén)RUE時(shí)),或是用以減去WAIT_OBJECT_0表示一個(gè)等待正常結束的互斥對象的索引(當fWaitAll為FALSE時(shí))。 下面給出的代碼主要展示了對WaitForMultipleObjects()函數的使用。通過(guò)對兩個(gè)事件內核對象的等待來(lái)控制線(xiàn)程任務(wù)的執行與中途退出:
// 存放事件句柄的數組
HANDLE hEvents[2];
UINT ThreadProc14(LPVOID pParam)
{
// 等待開(kāi)啟事件
DWORD dwRet1 = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE);
// 如果開(kāi)啟事件到達則線(xiàn)程開(kāi)始執行任務(wù)
if (dwRet1 == WAIT_OBJECT_0)
{
AfxMessageBox("線(xiàn)程開(kāi)始工作!");
while (true)
{
for (int i = 0; i < 10000; i++);
// 在任務(wù)處理過(guò)程中等待結束事件
DWORD dwRet2 = WaitForMultipleObjects(2, hEvents, FALSE, 0);
// 如果結束事件置位則立即終止任務(wù)的執行
if (dwRet2 == WAIT_OBJECT_0 + 1)
break;
}
}
AfxMessageBox("線(xiàn)程退出!");
return 0;
}
……
void CSample08View::OnStartEvent()
{
// 創(chuàng )建線(xiàn)程
for (int i = 0; i < 2; i++)
hEvents[i] = CreateEvent(NULL, FALSE, FALSE, NULL);
// 開(kāi)啟線(xiàn)程
AfxBeginThread(ThreadProc14, NULL);
// 設置事件0(開(kāi)啟事件)
SetEvent(hEvents[0]);
}
void CSample08View::OnEndevent()
{
// 設置事件1(結束事件)
SetEvent(hEvents[1]);
}
MFC為事件相關(guān)處理也提供了一個(gè)CEvent類(lèi),共包含有除構造函數外的4個(gè)成員函數PulseEvent()、ResetEvent()、SetEvent()和UnLock()。在功能上分別相當與Win32 API的PulseEvent()、ResetEvent()、SetEvent()和CloseHandle()等函數。而構造函數則履行了原CreateEvent()函數創(chuàng )建事件對象的職責,其函數原型為:
CEvent(BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );
按照此缺省設置將創(chuàng )建一個(gè)自動(dòng)復位、初始狀態(tài)為復位狀態(tài)的沒(méi)有名字的事件對象。封裝后的CEvent類(lèi)使用起來(lái)更加方便,圖2即展示了CEvent類(lèi)對A、B兩線(xiàn)程的同步過(guò)程:
圖2 CEvent類(lèi)對線(xiàn)程的同步過(guò)程示意
B線(xiàn)程在執行到CEvent類(lèi)成員函數Lock()時(shí)將會(huì )發(fā)生阻塞,而A線(xiàn)程此時(shí)則可以在沒(méi)有B線(xiàn)程干擾的情況下對共享資源進(jìn)行處理,并在處理完成后通過(guò)成員函數SetEvent()向B發(fā)出事件,使其被釋放,得以對A先前已處理完畢的共享資源進(jìn)行操作??梢?jiàn),使用CEvent類(lèi)對線(xiàn)程的同步方法與通過(guò)API函數進(jìn)行線(xiàn)程同步的處理方法是基本一致的。前面的API處理代碼可用CEvent類(lèi)將其改寫(xiě)為:
// MFC事件類(lèi)對象
CEvent g_clsEvent;
UINT ThreadProc22(LPVOID pParam)
{
// 對共享資源進(jìn)行寫(xiě)入操作
for (int i = 0; i < 10; i++)
{
g_cArray[i] = ‘a(chǎn)‘;
Sleep(1);
}
// 事件置位
g_clsEvent.SetEvent();
return 0;
}
UINT ThreadProc23(LPVOID pParam)
{
// 等待事件
g_clsEvent.Lock();
// 對共享資源進(jìn)行寫(xiě)入操作
for (int i = 0; i < 10; i++)
{
g_cArray[10 - i - 1] = ‘b‘;
Sleep(1);
}
return 0;
}
……
void CSample08View::OnEventMfc()
{
// 啟動(dòng)線(xiàn)程
AfxBeginThread(ThreadProc22, NULL);
AfxBeginThread(ThreadProc23, NULL);
// 等待計算完畢
Sleep(300);
// 報告計算結果
CString sResult = CString(g_cArray);
AfxMessageBox(sResult);
}
信號量?jì)群藢ο?div style="height:15px;">
信號量(Semaphore)內核對象對線(xiàn)程的同步方式與前面幾種方法不同,它允許多個(gè)線(xiàn)程在同一時(shí)刻訪(fǎng)問(wèn)同一資源,但是需要限制在同一時(shí)刻訪(fǎng)問(wèn)此資源的最大線(xiàn)程數目。在用CreateSemaphore()創(chuàng )建信號量時(shí)即要同時(shí)指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數設置為最大資源計數,每增加一個(gè)線(xiàn)程對共享資源的訪(fǎng)問(wèn),當前可用資源計數就會(huì )減1,只要當前可用資源計數是大于0的,就可以發(fā)出信號量信號。但是當前可用計數減小到0時(shí)則說(shuō)明當前占用資源的線(xiàn)程數已經(jīng)達到了所允許的最大數目,不能在允許其他線(xiàn)程的進(jìn)入,此時(shí)的信號量信號將無(wú)法發(fā)出。線(xiàn)程在處理完共享資源后,應在離開(kāi)的同時(shí)通過(guò)ReleaseSemaphore()函數將當前可用資源計數加1。在任何時(shí)候當前可用資源計數決不可能大于最大資源計數。
下面結合圖例3來(lái)演示信號量對象對資源的控制。在圖3中,以箭頭和白色箭頭表示共享資源所允許的最大資源計數和當前可用資源計數。初始如圖(a)所示,最大資源計數和當前可用資源計數均為4,此后每增加一個(gè)對資源進(jìn)行訪(fǎng)問(wèn)的線(xiàn)程(用黑色箭頭表示)當前資源計數就會(huì )相應減1,圖(b)即表示的在3個(gè)線(xiàn)程對共享資源進(jìn)行訪(fǎng)問(wèn)時(shí)的狀態(tài)。當進(jìn)入線(xiàn)程數達到4個(gè)時(shí),將如圖(c)所示,此時(shí)已達到最大資源計數,而當前可用資源計數也已減到0,其他線(xiàn)程無(wú)法對共享資源進(jìn)行訪(fǎng)問(wèn)。在當前占有資源的線(xiàn)程處理完畢而退出后,將會(huì )釋放出空間,圖(d)已有兩個(gè)線(xiàn)程退出對資源的占有,當前可用計數為2,可以再允許2個(gè)線(xiàn)程進(jìn)入到對資源的處理??梢钥闯?,信號量是通過(guò)計數來(lái)對線(xiàn)程訪(fǎng)問(wèn)資源進(jìn)行控制的,而實(shí)際上信號量確實(shí)也被稱(chēng)作Dijkstra計數器。
使用信號量?jì)群藢ο筮M(jìn)行線(xiàn)程同步主要會(huì )用到CreateSemaphore()、OpenSemaphore()、ReleaseSemaphore()、WaitForSingleObject()和WaitForMultipleObjects()等函數。其中,CreateSemaphore()用來(lái)創(chuàng )建一個(gè)信號量?jì)群藢ο?,其函數原型為?div style="height:15px;">