本系列意在記錄Windwos線程的相關知識點,包括線程基礎、線程調度、線程同步、TLS、線程池等
信號量內核對象
信號量內核對象用來進行資源計數,它包含一個使用計數、最大資源數、當前資源計數。最大資源數表示信號量可以控制的最大資源數量,當前資源數表示信號當前可用的資源數量。
設想一個場景:需要開發一個服務器進程,最多同時運行5個線程來響應客戶端請求,應該設計一個“線程池”。最開始的時候,5個線程都應該在等待狀態,如果有一個客戶端請求到來,那么喚醒其中的一個線程以處理客戶端請求,如果同時的請求數量為5,那么5個線程將全部投入使用,再多的請求應該被放棄。也就是說,隨着客戶端請求的增加,當前資源計數隨之遞減。
我們可能需要這樣的一個內核對象來實現這個功能:初始化5個線程並同時等待一個內核對象觸發,當一個客戶端請求到來時,試圖觸發內核對象,這樣5個線程中隨機一個被喚醒,並且自動使內核對象變為未觸發。外部判斷上限是否到達5。表面看來似乎用“自動重置的事件對象”即可實現這個功能啊,為什么要涉及到信號量呢?因為信號量還可以控制一次喚醒多少個線程!!而且這個例子只是信號量的一個用途,后面我們會看到一個更實際的用途。
總結一下,信號量內核對象是這樣的一種對象:它維護一個資源計數,當資源計數大於0,處於觸發狀態;資源計數等於0時,處於未觸發狀態;資源計數不可能小於0,也絕不可能大於資源計數上限。下圖展示了這種內核對象的特點:
如上圖,只有資源計數>0時才是觸發狀態,資源=0時為未觸發狀態,而WaitForSingleObject成功將遞減資源計數,調用ReleaseSemaphore將增加資源計數。
下面兩個函數CreateSemaphore和CreateSemaphoreEx用於創建信號量對象:
HANDLE WINAPI CreateSemaphore( __in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//內核對象安全描述符 __in LONG lInitialCount,//資源計數的初始值 __in LONG lMaximumCount,//資源計數的最大值 __in_opt LPCTSTR lpName //內核對象命名 ); HANDLE WINAPI CreateSemaphoreEx( __in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, __in LONG lInitialCount, __in LONG lMaximumCount, __in_opt LPCTSTR lpName, __reserved DWORD dwFlags, __in DWORD dwDesiredAccess );
任何進程可以用OpenSemaphore來得到一個命名的信號量:
HANDLE WINAPI OpenSemaphore( __in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in LPCTSTR lpName );
線程通過調用ReleaseSemaphore來遞增資源計數,不一定每次只遞增1,可以設置遞增任意值。當將要超過資源上限值的時候,ReleaseSemaphore會返回FALSE。
BOOL WINAPI ReleaseSemaphore( __in HANDLE hSemaphore, __in LONG lReleaseCount,//可以設置遞增的值 __out_opt LPLONG lpPreviousCount//返回先前的資源計數 );
互斥量內核對象
互斥量(mutex)用來確保一個線程獨占對一個資源的訪問。互斥量包含一個使用計數、線程ID和一個遞歸計數,互斥量與關鍵段的行為幾乎相同(因為它記錄了線程ID和遞歸計數,使得互斥量可以支持遞歸調用的情況)。互斥量的規則十分簡單:如果線程ID為0(即沒有線程獨占它),那么它處於觸發狀態,任何試圖等待該對象的線程都將獲得資源的獨占訪問;如果線程ID不為0,那么它處於未觸發狀態,任何試圖等待該對象的線程都將等待。
可以使用CreateMutex或者CreateMutexEx創建互斥對象:
HANDLE WINAPI CreateMutex( __in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, __in BOOL bInitialOwner,//初始化對象的狀態,如果傳入FALSE則會初始化為觸發狀態,如果傳入TRUE,那么對象的線程ID會被設置成當前調用線程,並初始化為未觸發 __in_opt LPCTSTR lpName ); HANDLE WINAPI CreateMutexEx( __in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, __in_opt LPCTSTR lpName, __in DWORD dwFlags, __in DWORD dwDesiredAccess );
一如既往,OpenMutex用於打開一個已經命名的互斥量內核對象:
HANDLE WINAPI OpenMutex( __in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in LPCTSTR lpName );
線程在獲得對獨占資源的訪問權限之后,可以正常執行相關的邏輯,當需要釋放互斥對象的時候可以調用ReleaseMutex:
BOOL WINAPI ReleaseMutex( __in HANDLE hMutex );
互斥量與其他內核對象不同,它會記錄究竟是哪個線程占用了共享資源,結合遞歸計數,同一個線程可以在獲得共享資源之后繼續訪問共享資源,這個行為就像關鍵段一樣。然而互斥量和關鍵段從本質上是不同的,關鍵段是用戶模式的線程同步方法,而互斥量是內核模式的線程同步方式。
介紹完這兩個內核對象后,我們思考一下前面在【Windows】線程漫談——線程同步之Slim讀/寫鎖中設計的一個場景:有一個共享的隊列,2個服務端線程負責讀取隊列中的條目以處理,2個客戶端線程負責寫入隊列中的條目以使服務先端線程處理,當隊列中沒有條目的時候應當掛起服務端線程,直到有條目進入時才被喚醒,另一方面,當隊列已滿時,客戶端線程應當掛起直到服務端至少處理了一個條目,以釋放至少一個條目的空間。
現在我們來用信號量和互斥量來實現同樣的功能,下面的流程圖分別是客戶端寫入線程和服務端讀取線程的邏輯:
1.首先創建一個互斥量對象m_hmtxQ,並初始化為未觸發狀態;之后創建一個信號量對象,並設置最大資源計數為隊列的長度,初始化資源計數為0,正好表征隊列元素的個數。
m_hmtxQ = CreateMutex(NULL,FALSE,NULL); m_hsemNumElements = CreateSemaphore(NULL,0,nMaxElements,NULL);
2.設計客戶端核心邏輯如下圖:
WatiForSingleObject:試圖獲得隊列的獨占訪問權限,對於這個隊列無論是讀還是寫都應該是線程獨占的。因此,使用互斥量對象來同步;
ReleaseSemaphore:試圖增加一個資源計數,表征客戶端想要向隊列中增加一個元素,當然隊列可能現在已經滿了,對應的資源計數已達到計數上限,此時ReleaseSemaphore會返回FALSE,這樣客戶端就不能像隊列中插入元素。反之,如果ReleaseSemaphore返回TRUE,表示隊列沒有滿,客戶端可以向隊列中插入元素。
ReleaseMutex:無論客戶端是否能夠像隊列中插入元素,在結束訪問后,都應該釋放互斥對象,以便其他線程能夠進入臨界資源。
3.設計服務端核心邏輯如下圖:
WatiForSingleObject:試圖獲得隊列的獨占訪問權限,對於這個隊列無論是讀還是寫都應該是線程獨占的。因此,使用互斥量對象來同步;
WaitForSingleObject(m_hsemNumElements…):試圖檢查信號量對象是否是觸發狀態。只有是觸發狀態的信號量對象,線程才能進入;也就意味着:隊列中只要有元素(資源>0,觸發狀態),服務端就能讀取。反之,如果隊列中沒有元素(資源=0,未觸發狀態),服務端將暫時不能訪問隊列,這時應該立即釋放Mutex。
ReleaseMutex:無論客戶端是否能夠像隊列中插入元素,在結束訪問后,都應該釋放互斥對象,以便其他線程能夠進入臨界資源。
勞動果實,轉載請注明出處:http://www.cnblogs.com/P_Chou/archive/2012/07/13/semaphore-and-mutex-in-thread-sync.html