本系列意在記錄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