Win32多线程编程(3) — 线程同步与通信
一.線程間數據通信
系統從進程的地址空間中分配內存給線程棧使用。新線程與創建它的線程在相同的進程上下文中運行。因此,新線程可以訪問進程內核對象的所有句柄、進程中的所有內存以及同一個進程中其他所有線程的棧。這樣一來,同一個進程中的多個線程可以很容易的相互通信。
到目前為止,將數據從一個線程傳到另一個線程的惟一方法是在創建線程時傳遞給新線程一個指針參數(LPVOIDlpParam)。參數lpParam為LPVOID指針類型,我們可在其中存儲普通的數值(size為平臺地址總線寬度),也可以存放指向某個數據結構(struct或class)的地址。在新線程函數中,解引用時需要強制類型轉換回原類型,以進行正確的訪問。
以下代碼段演示了一個典型的多線程場景。
// A typical multithread?scene
DWORD?WINAPI?FirstThread(PVOID?lpParam)
{
????// Initialize a stack-based variable
????int?x?= 0;
????DWORD?dwThreadID;
???
????// Create a new thread.
????HANDLE?hThread?=?CreateThread(NULL, 0, SecondThread, (LPVOID)&x, 0, &dwThreadID);
???
????// We don't reference the new thread anymore,
????// so close our handle to it.
????CloseHandle(hThread);
???
????// Our thread is done.
????// BUG:our stack will be destroyed,
????// but SecondThread might try to access it.
????return 0;
}
?
DWORD?WINAPI?SecondThread(LPVOID?lpParam)
{
????// Do some lengthy processing here.
????// ...
????// Attempt to access the variable on FirstThread's stack.
????// NOTE:This may cause an access violation - it depends on timing!
????*((int*)lpParam) = 5;
????// ...
????return 0;
}
上述場景中,Windows沒有維持線程之間的“父子關系“,即父線程FirstThread已經終止運行,而子線程SecondThread仍在繼續運行。以上父子關系只是為了一種解說上的方便,實際上FirstThread和SecondThread具有相同的優先級(默認是?normal),因此它們“同時”執行。這樣,FirstThread在開辟SecondThread后,不等SecondThread運行至代碼*((int*)lpParam) = 5;即可能退出。FirstThread棧上的自動變量x已銷毀,而SecondThread試圖去訪問之,將導致Access Violation。這是多線程編程中新手常犯的錯誤。
解決以上問題,大概有以下三種方案。
(1)讓創建線程等待新線程退出后才退出。在FirstThread中代碼CloseHandle(hThread);之前WaitForSingleObject(hThread,?INFINITE);這樣保證SecondThread中對FirstThread棧中自動變量x的訪問有效期。
(2)將x聲明為堆變量,即int *px?=?new?int;,在SecondThread中對px進行訪問完畢后調用deletepx;釋放堆內存。由于堆內存對進程有效,因此,上述代碼中FirstThread先退出,在SecondThread中對px的訪問依然有效,直到進程的某處將該內存delete掉。這是在需要動態創建線程參數(數據結構)時的一種解決方案,實際應用中經常用到。
(3)將x聲明為靜態變量static int?x?= 0;則將存儲在靜態存儲區域。這里有全局和局部之分,若在FirstThread之前聲明,則整個程序均可顯式訪問x;若在FirstThread之中聲明,則x只在FirstThread中可見。當然這里傳址給SecondThread,SecondThread可按址訪問。
在方案(3)中,若在FirstThread之中將x聲明為靜態變量,將使函數SecondThread不可重入。換言之,不能創建兩個使用相同線程函數的線程,因為這兩個線程將共享同一個靜態變量。這涉及到下文將要闡述的線程同步問題。
二.多線程同步互斥問題
1.同步問題的導入
多個線程共享數據時,同時讀沒有問題,但如果同時讀和寫,情況就不同了。
在本次線程內,?讀取一個變量時,為提高存取速度,編譯器優化時,有時會先把變量讀取到一個寄存器中;以后取變量值時,就直接從寄存器中取值;當變量值在本線程中改變時,會同時把變量的新值拷貝到該寄存器中,以便保持一致;當變量在因別的線程等而改變了值,該寄存器的值不會相應改變,從而造成應用程序讀取的值和實際的變量值不一致。
// CountError
#include?<stdio.h>
#include?<windows.h>
#include?<process.h>
?
int?g_nCount1?= 0;
int?g_nCount2?= 0;
BOOL?g_bContinue?=?TRUE;
?
UINT?__stdcall?ThreadFunc(LPVOID);
?
int?main(int?argc,?char*?argv[])
{
????UINT?uId;
????HANDLE?h[2];
???
????h[0] = (HANDLE)::_beginthreadex(NULL, 0,?ThreadFunc,?NULL, 0, &uId);
????h[1] = (HANDLE)::_beginthreadex(NULL, 0,?ThreadFunc,?NULL, 0, &uId);
?
????//?等待1秒后通知兩個計數線程結束,關閉句柄
????Sleep(1000);
????g_bContinue?=?FALSE;
?
?????//?等待兩個線程都運行完
????::WaitForMultipleObjects(2,?h,?TRUE,?INFINITE);
????::CloseHandle(h[0]);
????::CloseHandle(h[1]);
???
????printf("g_nCount1 = %d /n",?g_nCount1);
????printf("g_nCount2 = %d /n",?g_nCount2);
?
????return 0;
}
?
UINT?__stdcall?ThreadFunc(LPVOID)
{
????while(g_bContinue)
????{
???????g_nCount1++;
???????g_nCount2++;
????}
?
????return 0;
}
以上代碼中線程h[0]和h[1](具有相同的線程函數ThreadFunc)同時增加全局變量g_nCount1和g_nCount2的計數。按道理來說最終在主線程中輸出的它們的值應該是相同的,可是結果卻并不盡如人意。
上述測試中,g_nCount1和g_nCount2的值往往并不相同。出現此種結果主要是因為同時訪問g_nCount1和g_nCount2的兩個線程具有相同的優先級。在執行過程中,如果第一個線程取走g_nCount1的值準備進行自加操作的時候,它的時間片恰好用完,系統切換到第二個線程去對g_nCount1進行自加操作;一個時間片過后,第一個線程再次被調度,此時它會將上次取出的值自加,并放入g_nCount1所在的內存里,這就會覆蓋掉第二個線程對g_nCount1的自加操作。變量?g_nCount2也存在相同的問題。由于這樣的事情的發生次數是不可預知的,所以最終它們的值就不相同了。
針對以上問題,可使用volatile修飾靜態變量去優化,直接存取原始內存地址。對于volatile變量,優化器在用到這個變量時每次都必須小心地重新讀取這個變量的值,而不是使用保存在寄存器里的備份。這對于經常同硬件、中斷、RTOS等等打交道的嵌入式系統程序而言,是一種很好的解決方案。但常規情況下,很少使用去優化的volatile方式。
上例中,g_nCount1?和g_nCount2是全局變量,屬于該進程內所有線程共有的資源。解決問題的關鍵在于在一個線程對某個對象進行操作的過程中,需要有某種機制阻止其他線程的操作,這將涉及到到同步、互斥等話題。
2.同步與互斥的概念
同步與互斥往往像一對孿生兄弟,總是在同一語境中被提及,但往往語焉不詳。下面厘清一下它們之間的曖昧。
互斥:是指某一資源同時只允許一個訪問者對其進行訪問,具有唯一性和排它性。但互斥無法限制訪問者對資源的訪問順序,即訪問是無序的。
同步:是指在互斥的基礎上(大多數情況),通過其它機制實現訪問者對資源的有序訪問。在大多數情況下,同步已經實現了互斥,特別是所有寫入資源的情況必定是互斥的。少數情況是指可以允許多個訪問者同時訪問資源。
大街上的移動廁所往往只有一個茅坑,如果把廁所(茅坑)當做一種資源,則某一時刻,這種珍貴的資源只允許一人享用。這種對資源使用的獨占性和排他性即互斥。上述上廁所場景中,互斥這一原則的約束下,內急者先來先上,維持了良好的公共秩序。
從同步的制約性要素考慮,上廁所行為不存在“同步”問題。因為A上完廁所即可走人,接下來輪到B,B進行與A幾乎完全一樣的獨立操作,A和B也許素昧平生,它們之間不存在任何的制約關系。
關于同步的典型案例是“生產者-消費者”模型。生產者占用緩沖區時,消費者不能占用,反之亦然,這個即互斥;消費者必須要等生產者生產之后,才能消費,這個即同步。在這里,同步與互斥形影相隨,同步中暗含互斥。同時,可以看出,生產者和消費者的同步關系本質上是一種供需制約關系。
3.Win32多線程同步策略
多線程同步就要保證在一個線程占有公共資源的時候,其他線程不會再次占有這個資源。所以,解決同步問題,就是保證整個存取過程的獨占性。同步可以保證在一個時間內只有一個線程對某個共享資源有控制權,其本質是微觀串行所體現出來的等待。
之前談到的那個計數錯誤(Count Error)問題,涉及到如何協調線程間的活動,以保證對資源的正確訪問。Windows操作系統提供了多種同步手段,同步對象包括臨界區(Critical Section)、事件(Event)、信號量(Semaphore)、互斥量(Mutex)等。
(1)??臨界區對象(CRITICAL_SECTION),也稱關鍵代碼段
臨界區對象依賴一個CRITICAL_SECTION數據結構記錄一些信息,確保在同一時間只有一個線程訪問該數據段中的數據。
使用臨界區實施同步,首先需要聲明一個CRITICAL_SECTION?結構。對CRITICAL_SECTION?結構的操作包括Initialize、Enter/Leave、Delete。
編程的時候,要把臨界區對象定義在想保護的數據段中,然后在任何線程使用此臨界區對象之前,調用InitializeCriticalSection函數對它進行初始化。
// The InitializeCriticalSection function initializes a critical section object.
VOID?InitializeCriticalSection(
????????????????????????????LPCRITICAL_SECTION?lpCriticalSection??// critical section
????????????????????????????);
之后,線程訪問臨界區中數據的時候,必須首先調用?EnterCriticalSection?函數,申請進入臨界區。在同一時間內,Windows只允許一個線程進入臨界區。所以在申請的時候,如果有另一個線程在臨界區的話,EnterCriticalSection函數會一直等待下去,直到其他線程離開臨界區才返回。
// The EnterCriticalSection function waits for ownership of the specified critical section object. The function returns when the calling thread is granted ownership.
VOID?EnterCriticalSection(
???????????????????????LPCRITICAL_SECTION?lpCriticalSection??// critical section
???????????????????????);
????當操作完成的時候,要調用LeaveCriticalSection函數將臨界區交還給Windows系統,以便其他線程可以申請使用。否則,就是占著茅坑不拉屎憋死其他人的不道德行為。
// The LeaveCriticalSection function releases ownership of the specified critical section object.
VOID?LeaveCriticalSection(
???????????????????????LPCRITICAL_SECTION?lpCriticalSection???// critical section
???????????????????????);
當程序不再使用臨界區對象的時候,必須使用DeleteCriticalSection函數執行刪除操作,釋放資源。
// The DeleteCriticalSection function releases all resources used by an unowned critical section object.
VOID?DeleteCriticalSection(
????????????????????????LPCRITICAL_SECTION?lpCriticalSection???// critical section
????????????????????????);
聲明一個CRITICAL_SECTION對象,即政府修建了一座廁所。廁所這一資源起初是上鎖的,Initialize可看做廁所開鎖對外開放,使廁所可用。Enter可看做上廁所的排隊過程,一旦茅坑空出,即可持票進入享用;Leave可看做如廁完畢沖水走人。廁所被很多人Enter/Leave用了幾年后,其使命結束,政府依據城市規劃,將其拆掉—Delete收回。
臨界區對象能夠很好地保護共享數據,但是它不能夠用于進程之間資源的鎖定。由于它不是內核對象,故臨界區只能用于在同一進程內的線程同步。如果要在進程間維持線程的同步,可以使用事件內核對象。
(2)???????事件內核對象(event)
多線程程序設計大多會涉及線程間相互通信。主線程在創建工作線程的時候,可以通過參數給工作線程傳遞初始化數據,當工作線程開始運行后,還需要通過通信機制來控制工作線程。同樣,工作線程有時候也需要將一些情況主動通知主線程。事件內核對象是一種比較好的通信方法。
事件對象(event)是一種抽象的對象,它也有未受信(nonsignaled)和受信(signaled)兩種狀態。編程人員也可以使用WaitForSingleObject/WaitForMultipleObjects函數等待其變成受信狀態。不同于其他內核對象,系統提供的一些API可以使事件對象在這兩種狀態之間轉化??梢园咽录ο罂闯墒且粋€設置在Windows?內部的標志,它的狀態設置和測試工作由Windows來完成。
事件對象包含?3?個成員:nUsageCount(使用計數)、bManualReset(是否人工重置)和bSignaled(是否受信)。成員nUsageCount記錄了當前的使用計數,當使用計數為?0?的時候,Windows?就會銷毀此內核對象占用的資源;成員bManualReset指定在一個事件內核對象上等待的函數返回之后,Windows是否重置這個對象為未受信狀態;成員bSignaled?指定當前事件內核對象是否受信。下面要介紹的操作事件內核對象的函數會影響這些成員的值。
要使用事件對象,首先用CreateEvent函數去創建它。
// The CreateEvent function creates or opens a named or unnamed event object.
HANDLE?CreateEvent(
?????????????????LPSECURITY_ATTRIBUTES?lpEventAttributes,?// SD
?????????????????BOOL?bManualReset,???????????????????????// reset type
?????????????????BOOL?bInitialState,??????????????????????// initial state
?????????????????LPCTSTR?lpName???????????????????????????// object name
);
參數一為事件對象的安全屬性,一般填充NULL表示取默認值。
參數二,選擇事件對象的重置方式以決定類型。bManualReset?=?TRUE則表示人工重置(manual-reset);bManualReset?=?FALSE則表示自動重置(auto-reset)。當一個人工重置的事件對象受信以后,所有等待在這個事件上的線程都會變為可調度狀態;當一個自動重置的事件對象受信以后,Windows?僅允許一個等待在該事件上的線程變成可調度狀態,然后就自動重置此事件對象為未受信狀態。通常使用自動重置的事件內核對象,即設置bManualReset?=?FALSE。
參數三bInitialState對應著?bSignaled?成員的初態。若將它設為TRUE,則表示事件對象創建時的初始狀態為受信;若將它設為FALSE,則初始狀態為未受信。通常設置初始狀態為未受信,即置bInitialState?=?FALSE。
參數四指定事件對象的名稱,以便跨進程按名訪問。
跨進程訪問事件內核對象,可傳入事件對象名調用OpenEvent獲取該對象的句柄。
// The OpenEvent function opens an existing named event object.
HANDLE?OpenEvent(
???????????????DWORD?dwDesiredAccess,??// access
???????????????BOOL?bInheritHandle,????// inheritance option
???????????????LPCTSTR?lpName??????????// object name
);
系統創建或打開一個事件內核對象后,會返回事件的句柄。當編程人員不使用此內核對象的時候,應該調用CloseHandle函數釋放它占用的資源。
事件對象被建立后,程序可以通過SetEvent和ResetEvent函數來設置它的狀態。
// The SetEvent function sets the specified event object to the signaled state.
BOOL?SetEvent(
?????????????HANDLE?hEvent???// handle to event
);
// The ResetEvent function sets the specified event object to the nonsignaled state.
BOOL?ResetEvent(
??????????????HANDLE?hEvent???// handle to event
);
與SetEvent/ResetEvent相關的另一個函數是PulseEvent。顧名思義,所謂PulseEvent即瞬間Set/Reset,至于這個瞬間多長及其效用,這里不詳解。可參考《關于線程同步?PulseEvent()》
// The PulseEvent function sets the specified event object to the signaled state and then resets it to the nonsignaled state after releasing the appropriate number of waiting threads.
BOOL?PulseEvent(
??????????????HANDLE?hEvent???// handle to event object
??????????????);
事件內核對象的同步,代碼上體現在對WaitForSingleObject/WaitForMultipleObjects函數的調用,等待事件對象的置信。我們可以將之想象為廁所門外的“有人/沒人”信號燈,紅燈有人(nonsignaled),綠燈無人(signaled)。
事件對象是一個用于線程間通信被廣泛使用的內核對象。因為它是一個內核對象,所以也可以跨進程使用。依靠在線程間通信就可以使各線程的工作協調進行,達到同步的目的。
(3)??????信號量內核對象(Semaphore)
信號量內核對象對線程的同步方式與前面幾種不同,它允許多個線程在同一時刻訪問某一資源,但是需要限制同一時刻訪問此資源的最大線程數目。
首先使用CreateSemaphore函數創建信號量內核對象。
// The CreateSemaphore function creates or opens a named or unnamed semaphore object.
HANDLE?CreateSemaphore(
?????????????????????LPSECURITY_ATTRIBUTES?lpSemaphoreAttributes,?// SD
?????????????????????LONG?lInitialCount,??????????// initial count
?????????????????????LONG?lMaximumCount,??????????// maximum count
?????????????????????LPCTSTR?lpName????????????????// object name
?????????????????????);
CreateSemaphore函數創建信號量時,參數三指定允許的最大資源計數,參數二指定當前可用的初始資源計數。一般將lInitialCount設置與lMaximumCount相等。
參數四即內核對象名稱,以便跨進程執行OpenSemaphore按名訪問。
只要當前可用資源計數大于0,就可以發出信號量信號,在該信號量上的等待函數調用WaitForSingleObject返回。每增加一個線程對共享資源的訪問,當前可用資源計數就會減1。WaitForSingleObject返回后,調用線程在對共享資源的同步處理完畢后,應調用ReleaseSemaphore來增加當前可用資源計數。否則,將會出現當前正在處理共享資源的實際線程并沒有達到要限制的數值,而其他線程卻因為當前可用資源計數為0而仍無法進入的情況。
// The ReleaseSemaphore function increases the count of the specified semaphore object by a specified amount.
BOOL?ReleaseSemaphore(
????????????????????HANDLE?hSemaphore,???????// handle to semaphore
????????????????????LONG?lReleaseCount,??????// count increment amount
????????????????????LPLONG?lpPreviousCount???// previous count
????????????????????);
參數一為信號量內核對象句柄;參數二為計數遞增值,一般設為1,當然也可以按需要設置大于1的值;參數三為之前的計數,往往填NULL,當然可指定導出到當地變量。
信號量內核對象的同步,代碼上體現在對WaitForSingleObject/WaitForMultipleObjects函數的調用,其同步條件為資源計數大于0,即有可用資源。某個線程處理完共享資源后,需要調用ReleaseSemaphore釋放資源,增加可用資源計數。當然,同其他內核對象一樣,最終也得調用CloseHandle函數釋放信號量內核對象占用的資源。
不同于過于簡陋的只有一個茅坑的移動廁所,豪華一點的公共廁所往往不止一個坑位。將坑位看做資源,則坑位的個數即信號量機制中的資源計數。這一排坑位,在同一時間內也只能滿足部分人的需求,一個人一個坑,有人用完了釋放坑位,排隊的人才有機會進入。
信號量的使用特點使其更適用于對Socket程序中線程的同步問題。一個典型的場景就是HTTP服務器要對同一時間內訪問同一頁面的用戶數加以限制,這是可以為每一個用戶對服務器的頁面請求設置一個線程,而頁面則是待保護的資源,通過使用信號量對線程的同步作用可以確保在任一時刻無論有多少用戶對某一頁面進行訪問,只有不大于設置的最大用戶數目的線程能夠進行訪問,而其他訪問企圖被掛起。只有在有用戶退出對此頁面的訪問后,其他用戶的訪問請求才有可能得到響應。
迅雷的“原始地址線程數”即是設置客戶端從某一原始地址下載資源的最大線程數。當然,資源所在的站點本身就會對某一客戶連接數有限制,這里的“某一客戶連接數”意即把文件拆開,一個線程下載一塊的多線程協助下載。
當然,多線程并不是越多越好,迅雷下載肯定使用的是線程池。迅雷將下載線程數限制為5,符合線程池的經驗公式,即線程池規模?= CPU數?* 2 + 1,現在機器基本都是雙核或多CPU的。當用戶建立5個以上的下載任務時,迅雷最多同時執行5個下載任務,超出的任務將排隊等待。一旦有下載任務完成,另一個等待下載任務即啟動。迅雷對于下載線程數的限制,即使用了信號量機制。
(4)??????互斥內核對象(Mutex)
互斥是一種用途非常廣泛的內核對象。能夠保證多個線程對同一共享資源的互斥訪問。同臨界區有些類似,只有擁有互斥對象的線程才具有訪問資源的權限。由于互斥對象只有一個,因此就決定了任何情況下,此共享資源都不會被多個線程所訪問。當前占據資源的線程在任務處理完后應該將占據的互斥對象交出,以便其他線程在其上的等待調用WaitForSingleObject/WaitForMultipleObjects返回。
基于互斥內核對象來保持線程同步用到的函數主要有CreateMutex、OpenMutex、ReleaseMutex,其用法在代碼布局上同信號量內核對象。
// The CreateMutex function creates or opens a named or unnamed mutex object.
HANDLE?CreateMutex(
?????????????????LPSECURITY_ATTRIBUTES?lpMutexAttributes,??// SD
?????????????????BOOL?bInitialOwner,????????????????// initial owner
?????????????????LPCTSTR?lpName??????????????????????// object name
?????????????????);
參數bInitialOwner主要用來控制互斥對象的初始狀態,一般將其設為FALSE,以表明互斥對象在創建時并沒有為任何線程所占有。最后一個參數即內核對象名稱,以便跨進程執行OpenMutex按名訪問。
當目前對資源具有訪問權限的線程不再需要訪問此資源而要離開時,必須通過ReleaseMutex函數來釋放其擁有的互斥對象。
// The ReleaseMutex function releases ownership of the specified mutex object.
BOOL?ReleaseMutex(
????????????????HANDLE?hMutex???// handle to mutex
????????????????);???
基于互斥內核對象的同步在代碼上體現在對WaitForSingleObject/WaitForMultipleObjects函數的調用,以等待互斥內核對象的通知,其同步條件為某一時刻只有一個線程擁有互斥對象。
在互斥對象通知引起調用等待函數返回時,等待函數的返回值不在是WAIT_OBJECT_0或[WAIT_OBJECT_0,WAIT_OBJECT_0+nCount-1]之間的某值,而是將返回WAIT_ABANDONED_0或是[WAIT_ABANDONED_0,WAIT_ABANDONED_0+nCount-1]?之間的某值,以此來表明線程正在等待的互斥對象由另外一個線程所擁有,而此線程卻在使用完共享資源前就已經終止。除此之外,使用互斥內核對象的方法在等待線程的可調度性上同使用其他幾種內核對象的方法也有所不同,其他內核對象在沒有得到通知時,受調用等待函數的作用,線程將會掛起,同時喪失可調度性,而使用互斥的方法可以在等待的同時仍具有調度性,這也正是互斥對象所能完成的非常規操作之一。
在編寫程序時,互斥對象多用在對那些為多個線程所訪問的內存塊的保護上,可以確保任何線程在處理此內存快時,都對其擁有可靠的獨占訪問權。
(5)??????其他(互鎖函數和旋轉鎖)
互鎖函數
互鎖函數為同步訪問多線程共享變量提供了一個簡單的機制。如果變量在共享內存,不同進程的線程也可以使用此機制。用于互鎖的函數有?InterlockedIncrement、?InterlockedDecrement等。
InterlockedIncrement?函數遞增(加?1)指定的?32?位變量。這個函數可以阻止其他線程同時使用此變量,函數原型如下。
// The InterlockedIncrement function increments (increases by one) the value of the specified variable and checks the resulting value. The function prevents more than one thread from using the same variable simultaneously.
LONG?InterlockedIncrement(
???????????????????????LPLONG?volatile?lpAddend???// variable to increment
???????????????????????);
InterlockedDecrement函數同步遞減(減1)指定的32位變量,原型如下。
// The InterlockedDecrement function decrements (decreases by one) the value of the specified variable and checks the resulting value. The function prevents more than one thread from using the same variable simultaneously.
LONG?InterlockedDecrement(
???????????????????????LPLONG?volatile?lpAddend???// variable address
???????????????????????);
Interlocked系列函數以原子方式操控一個值。所謂原子操作,即在多進程(線程)的操作系統中不能被其它進程(線程)打斷的操作就。它的實現,取決于代碼運行的CPU平臺。如果是x86系列CPU,則Interlocked函數會在總線上維持一個硬件信號,這個信號會阻止其他CPU訪問同一內存地址。
InterlockedExchangeAdd函數可以替代InterlockedIncrement/InterlockedDecrement,可以加減任何值。InterlockedExchange和InterlockedExchangePointer函數可實現原子級別的賦值(訪問)操作。它們在實現旋轉鎖時極其有用。
旋轉鎖
為了提高臨界區的性能,Microsoft把旋轉鎖合并到了臨界區中。因此當調用EnterCriticalSection的時候,它會用一個旋轉鎖不斷地循環,嘗試在一段時間內獲得對資源的訪問權。只有當嘗試失敗的時候,線程才會切換到內核模式并進入等待狀態。
為了使用臨界區的時候同時使用旋轉鎖,Windows提供了一套在臨界區基礎上添加SPIN計數的API,使用InitializeCriticalSectionAndSpinCount函數替代InitializeCriticalSection,使用SetCriticalSectionSpinCount進行SPIN COUNT的設置。
旋轉鎖假定被保護的資源只會被占用一小段時間。與切換到內核模式然后等待相比,這種優先用戶模式下的旋轉等待效率會更高。只要在經歷了指定次數(SpinCount = 4000)后,仍然無法訪問資源時,線程才會真正切換到內核模式,并一直等待到資源可供使用為止(此時,它不消耗CPU時間)。這就是臨界區(CRITICAL_SECTION)的實現方式。
(6)??????同步機制的比較
在同步技術中,臨界區是最容易掌握的。它是一種簡單的數據結構。同通過等待和釋放內核態互斥對象實現同步的方式相比,臨界區的速度明顯勝出。但是臨界區非內核對象,不能用在多進程間的線程同步,只能用于單個進程內部的線程同步。但是,對于普通的單進程程序,鑒于臨界區使用旋轉鎖優先在用戶模式同步的高效性,使用臨界區不失為一種普遍的同步方案。
事件內核對象是一種基本的內核對象,被廣泛使用于多進程、多線程同步通信。實際上,臨界區RTL_CRITICAL_SECTION內部即使用了事件內核對象。
信號量機制在內核模式工作,適用于允許特定個數的線程執行某任務。
互斥對象與其他內核對象不同,互斥對象在操作系統中擁有特殊代碼,并由操作系統來管理。
關于跨進程多線程的高速同步,參考《Windows內核編程》第四版第10章《線程同步工具包》。Jeffrey Richter將自己改良的臨界區封裝成一個COptex類,只有在必要時才進入核心態,盡可能在用戶態同步,從而實現對同步的優化。可參考飛鴿傳書程序員蔡鎮定的博文《使用臨界段實現優化的進程間同步對象》
參考:
《VC中利用多線程技術實現線程之間的通信》
總結
以上是生活随笔為你收集整理的Win32多线程编程(3) — 线程同步与通信的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win32多线程编程(2) — 线程控制
- 下一篇: Win32多线程编程(5) — 线程局部