【Windows】線程漫談——線程同步之信號量和互斥量


本系列意在記錄Windwos線程的相關知識點,包括線程基礎、線程調度、線程同步、TLS、線程池等

 

信號量內核對象

信號量內核對象用來進行資源計數,它包含一個使用計數、最大資源數、當前資源計數。最大資源數表示信號量可以控制的最大資源數量,當前資源數表示信號當前可用的資源數量。

設想一個場景:需要開發一個服務器進程,最多同時運行5個線程來響應客戶端請求,應該設計一個“線程池”。最開始的時候,5個線程都應該在等待狀態,如果有一個客戶端請求到來,那么喚醒其中的一個線程以處理客戶端請求,如果同時的請求數量為5,那么5個線程將全部投入使用,再多的請求應該被放棄。也就是說,隨着客戶端請求的增加,當前資源計數隨之遞減。

我們可能需要這樣的一個內核對象來實現這個功能:初始化5個線程並同時等待一個內核對象觸發,當一個客戶端請求到來時,試圖觸發內核對象,這樣5個線程中隨機一個被喚醒,並且自動使內核對象變為未觸發。外部判斷上限是否到達5。表面看來似乎用“自動重置的事件對象”即可實現這個功能啊,為什么要涉及到信號量呢?因為信號量還可以控制一次喚醒多少個線程!!而且這個例子只是信號量的一個用途,后面我們會看到一個更實際的用途。

總結一下,信號量內核對象是這樣的一種對象:它維護一個資源計數,當資源計數大於0,處於觸發狀態;資源計數等於0時,處於未觸發狀態;資源計數不可能小於0,也絕不可能大於資源計數上限。下圖展示了這種內核對象的特點:

image

如上圖,只有資源計數>0時才是觸發狀態,資源=0時為未觸發狀態,而WaitForSingleObject成功將遞減資源計數,調用ReleaseSemaphore將增加資源計數。

下面兩個函數CreateSemaphoreCreateSemaphoreEx用於創建信號量對象:

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.設計客戶端核心邏輯如下圖:

image

WatiForSingleObject:試圖獲得隊列的獨占訪問權限,對於這個隊列無論是讀還是寫都應該是線程獨占的。因此,使用互斥量對象來同步;

ReleaseSemaphore:試圖增加一個資源計數,表征客戶端想要向隊列中增加一個元素,當然隊列可能現在已經滿了,對應的資源計數已達到計數上限,此時ReleaseSemaphore會返回FALSE,這樣客戶端就不能像隊列中插入元素。反之,如果ReleaseSemaphore返回TRUE,表示隊列沒有滿,客戶端可以向隊列中插入元素。

ReleaseMutex:無論客戶端是否能夠像隊列中插入元素,在結束訪問后,都應該釋放互斥對象,以便其他線程能夠進入臨界資源。

3.設計服務端核心邏輯如下圖:

image

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM