程序/執行緒同步——Critical Section,Mutex,Semaphore,Event區別
轉自:http://blog.163.com/around-wind/blog/static/271230752009084037565/
臨界區(Critical Section)
保證在某一時刻只有一個執行緒能訪問資料的簡便辦法。在任意時刻只允許一個執行緒對共享資源進行訪問。如果有多個執行緒試圖同時訪問臨界區,那麼在有一個執行緒進 入後其他所有試圖訪問此臨界區的執行緒將被掛起,並一直持續到進入臨界區的執行緒離開。臨界區在被釋放後,其他執行緒可以繼續搶佔,並以此達到用原子方式操作共 享資源的目的。
臨界區包含兩個操作原語: EnterCriticalSection() 進入臨界區 LeaveCriticalSection() 離開臨界區
EnterCriticalSection()語句執行後代碼將進入臨界區以後無論發生什麼,必須確保與之匹配的 LeaveCriticalSection()都能夠被執行到。否則臨界區保護的共享資源將永遠不會被釋放。在使用臨界區時,一般不允許其執行時間過長, 只要進入臨界區的執行緒還沒有離開,其他所有試圖進入此臨界區的執行緒都會被掛起而進入到等待狀態,並會在一定程度上影響。程式的執行效能。尤其需要注意的是 不要將等待使用者輸入或是其他一些外界干預的操作包含到臨界區。如果進入了臨界區卻一直沒有釋放,同樣也會引起其他執行緒的長時間等待。換句話說,在執行了 EnterCriticalSection()語句進入臨界區後無論發生什麼,必須確保與之匹配的LeaveCriticalSection()都能夠被 執行到。可以通過新增結構化異常處理程式碼來確保LeaveCriticalSection()語句的執行。雖然臨界區同步速度很快,但卻只能用來同步本進 程內的執行緒,而不可用來同步多個程序中的執行緒。
MFC提供了很多功能完備的類,我用MFC實現了臨界區。MFC為臨界區提供有一個CCriticalSection類,使用該類進行執行緒同步處理是非常 簡單的。只需線上程函式中用CCriticalSection類成員函式Lock()和UnLock()標定出被保護程式碼片段即可。Lock()後代碼用 到的資源自動被視為臨界區內的資源被保護。UnLock後別的執行緒才能訪問這些資源。
互斥量(Mutex)
互斥(Mutex)是一種用途非常廣泛的核心物件。能夠保證多個執行緒對同一共享資源的互斥訪問。同臨界區有些類似,只有擁有互斥物件的執行緒才具有訪問資源 的許可權,由於互斥物件只有一個,因此就決定了任何情況下此共享資源都不會同時被多個執行緒所訪問。當前佔據資源的執行緒在任務處理完後應將擁有的互斥物件交 出,以便其他執行緒在獲得後得以訪問資源。與其他幾種核心物件不同,互斥物件在作業系統中擁有特殊程式碼,並由作業系統來管理,作業系統甚至還允許其進行一些 其他核心物件所不能進行的非常規操作。 互斥量跟臨界區很相似,只有擁有互斥物件的執行緒才具有訪問資源的許可權,由於互斥物件只有一個,因此就決定了任何情況下此共享資源都不會同時被多個執行緒所訪 問。當前佔據資源的執行緒在任務處理完後應將擁有的互斥物件交出,以便其他執行緒在獲得後得以訪問資源。互斥量比臨界區複雜。因為使用互斥不僅僅能夠在同一應 用程式不同執行緒中實現資源的安全共享,而且可以在不同應用程式的執行緒之間實現對資源的安全共享。
以互斥核心物件來保持執行緒同步可能用到的函式主要有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()函式)。以此來表明執行緒正在等 待的互斥物件由另外一個執行緒所擁有,而此執行緒卻在使用完共享資源前就已經終止。除此之外,使用互斥物件的方法在等待執行緒的可排程性上同使用其他幾種核心對 象的方法也有所不同,其他核心物件在沒有得到通知時,受呼叫等待函式的作用,執行緒將會掛起,同時失去可排程性,而使用互斥的方法卻可以在等待的同時仍具有 可排程性,這也正是互斥物件所能完成的非常規操作之一。
在編寫程式時,互斥物件多用在對那些為多個執行緒所訪問的記憶體塊的保護上,可以確保任何執行緒在處理此記憶體塊時都對其擁有可靠的獨佔訪問權。
互斥物件在MFC中通過CMutex類進行表述。使用CMutex類的方法非常簡單,在構造CMutex類物件的同時可以指明待查詢的互斥物件的名字,在 建構函式返回後即可訪問此互斥變數。CMutex類也是隻含有建構函式這唯一的成員函式,當完成對互斥物件保護資源的訪問後,可通過呼叫從父類 CSyncObject繼承的UnLock()函式完成對互斥物件的釋放。CMutex類建構函式原型為:
CMutex( BOOL bInitiallyOwn = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );
該類的適用範圍和實現原理與API方式建立的互斥核心物件是完全類似的,但要簡潔的多。
Mutex和Critical Section都是主要用於限制多執行緒(Multithread)對全域性或共享的變數、物件或記憶體空間的訪問。下面是其主要的異同點(不同的地方用綠色表示)。
Mutex |
Critical Section |
|
效能和速度 |
慢。 Mutex 是核心物件,相關函式的執行 (WaitForSingleObject, ReleaseMutex)需要使用者模式(User Mode)到核心模式(Kernel Mode)的轉換,在x86處理器上這種轉化一般要發費600個左右的 CPU指令週期。 |
快。 Critical Section本身不是核心物件,相關函式(EnterCriticalSection,LeaveCriticalSection)的呼叫一般都在使用者模式內執行,在x86處理器上一般只需要發費9個左右的 CPU指令週期。只有當想要獲得的鎖正好被別的執行緒擁有時才會退化成和Mutex一樣,即轉換到核心模式,發費600個左右的 CPU指令週期。 |
能否跨越程序(Process)邊界 |
可以 |
不可 |
定義寫法 |
HANDLE hmtx; |
CRITICAL_SECTION cs; |
初始化寫法 |
hmtx= CreateMutex (NULL, FALSE, NULL); |
InitializeCriticalSection(&cs); |
結束清除寫法 |
CloseHandle(hmtx); |
DeleteCriticalSection(&cs); |
無限期等待的寫法 |
WaitForSingleObject (hmtx, INFINITE); |
EnterCriticalSection(&cs); |
0等待(狀態檢測)的寫法 |
WaitForSingleObject (hmtx, 0); |
TryEnterCriticalSection(&cs); |
任意時間等待的寫法 |
WaitForSingleObject (hmtx, dwMilliseconds); |
不支援 |
鎖釋放的寫法 |
ReleaseMutex(hmtx); |
LeaveCriticalSection(&cs); |
能否被一道用於等待其他核心物件 |
可以(使用WaitForMultipleObjects,WaitForMultipleObjectsEx,MsgWaitForMultipleObjects,MsgWaitForMultipleObjectsEx等等) |
不可 |
當擁有鎖的執行緒死亡時 |
Mutex變成abandoned狀態,其他的等待執行緒可以獲得鎖。 |
Critical Section的狀態不可知(undefined),以後的動作就不能保證了。 |
自己會不會鎖住自己 |
不會(對已獲得的Mutex,重複呼叫WaitForSingleObject不會鎖住自己。但最後你別忘了要呼叫同樣次數的ReleaseMutex) |
不會(對已獲得的Critical Section,重複呼叫EnterCriticalSection不會鎖住自己。但最後你別忘了要呼叫同樣次數的LeaveCriticalSection) |
訊號量(Semaphores)
訊號量物件對執行緒的同步方式與前面幾種方法不同,訊號允許多個執行緒同時使用共享資源,這與作業系統中的PV操作相同。它指出了同時訪問共享資源的執行緒最大 數目。它允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目。在用CreateSemaphore()建立訊號量時即 要同時指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數設定為最大資源計數,每增加一個執行緒對共享資源的訪問,當前可用資源計數就會 減1,只要當前可用資源計數是大於0的,就可以發出訊號量訊號。但是當前可用計數減小到0時則說明當前佔用資源的執行緒數已經達到了所允許的最大數目,不能 在允許其他執行緒的進入,此時的訊號量訊號將無法發出。執行緒在處理完共享資源後,應在離開的同時通過ReleaseSemaphore()函式將當前可用資 源計數加1。在任何時候當前可用資源計數決不可能大於最大資源計數。 訊號量是通過計數來對執行緒訪問資源進行控制的,而實際上訊號量確實也被稱作Dijkstra計數器。
PV操作及訊號量的概念都是由荷蘭科學家E.W.Dijkstra提出的。訊號量S是一個整數,S大於等於零時代表可供併發程序使用的資源實體數,但S小於零時則表示正在等待使用共享資源的程序數。
P操作申請資源:
(1)S減1;
(2)若S減1後仍大於等於零,則程序繼續執行;
(3)若S減1後小於零,則該程序被阻塞後進入與該訊號相對應的佇列中,然後轉入程序排程。
V操作 釋放資源:
(1)S加1;
(2)若相加結果大於零,則程序繼續執行;
(3)若相加結果小於等於零,則從該訊號的等待佇列中喚醒一個等待程序,然後再返回原程序繼續執行或轉入程序排程。
使用訊號量核心物件進行執行緒同步主要會用到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伺服器要對同一時間內訪問同一頁面的使用者數加以限制,這時可以為沒一個使用者對伺服器的頁面請求設定一個執行緒,而頁面則是待保護的共享資源,通過 使用訊號量對執行緒的同步作用可以確保在任一時刻無論有多少使用者對某一頁面進行訪問,只有不大於設定的最大使用者數目的執行緒能夠進行訪問,而其他的訪問企圖則 被掛起,只有在有使用者退出對此頁面的訪問後才有可能進入。
在MFC中,通過CSemaphore類對訊號量作了表述。該類只具有一個建構函式,可以構造一個訊號量物件,並對初始資源計數、最大資源計數、物件名和安全屬性等進行初始化,其原型如下:
CSemaphore( LONG lInitialCount = 1, LONG lMaxCount = 1, LPCTSTR pstrName = NULL, LPSECURITY_ATTRIBUTES lpsaAttributes = NULL );
在構造了CSemaphore類物件後,任何一個訪問受保護共享資源的執行緒都必須通過 CSemaphore從父類CSyncObject類繼承得到的Lock()和UnLock()成員函式來訪問或釋放CSemaphore物件。與前面介 紹的幾種通過MFC類保持執行緒同步的方法類似,通過CSemaphore類也可以將前面的執行緒同步程式碼進行改寫,這兩種使用訊號量的執行緒同步方法無論是在 實現原理上還是從實現結果上都是完全一致的。
事件(Event)
事件物件也可以通過通知操作的方式來保持執行緒的同步。並且可以實現不同程序中的執行緒同步操作。
訊號量包含的幾個操作原語:
CreateEvent() 建立一個訊號量
OpenEvent() 開啟一個事件
SetEvent() 回置事件
WaitForSingleObject() 等待一個事件
WaitForMultipleObjects() 等待多個事件
使用臨界區只能同步同一程序中的執行緒,而使用事件核心物件則可以對程序外的執行緒進行同步,其前提是得到對此事件物件的訪問權。可以通過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時)。
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類使用起來更加方便,
事件可以實現不同程序中的執行緒同步操作,並且可以方便的實現多個執行緒的優先比較等待操作,例如寫多個WaitForSingleObject來代替WaitForMultipleObjects從而使程式設計更加靈活。
總結:
1. 互斥量與臨界區的作用非常相似,但互斥量是可以命名的,也就是說它可以跨越程序使用。所以建立互斥量需要的資源更多,所以如果只為了在程序內部是用的話使 用臨界區會帶來速度上的優勢並能夠減少資源佔用量。因為互斥量是跨程序的互斥量一旦被建立,就可以通過名字開啟它。
2. 互斥量(Mutex),訊號燈(Semaphore),事件(Event)都可以被跨越程序使用來進行同步資料操作,而其他的物件與資料同步操作無關,但 對於程序和執行緒來講,如果程序和執行緒在執行狀態則為無訊號狀態,在退出後為有訊號狀態。所以可以使用WaitForSingleObject來等待程序和 執行緒退出。
3. 通過互斥量可以指定資源被獨佔的方式使用,但如果有下面一種情況通過互斥量就無法處理,比如現在一位使用者購買了一份三個併發訪問許可的資料庫系統,可以根 據使用者購買的訪問許可數量來決定有多少個執行緒/程序能同時進行資料庫操作,這時候如果利用互斥量就沒有辦法完成這個要求,訊號燈物件可以說是一種資源計數 器。