【Windows】線程漫談——線程同步之Slim讀/寫鎖


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

 

Slim讀/寫鎖

SRWLock的目的和關鍵段相同,對一個資源進行保護,構造了一段“原子訪問”的代碼,不讓其他線程訪問它。但與關鍵段不同的是SRWLock允許區分想要讀取資源值的線程和想要寫入資源值的線程,因為僅僅讀取資源是不會破壞數據的,下面是Slim讀/寫鎖的簡單用法:

SRWLOCK g_srwLock
...
//init SRWLock
InitializeSRWLock(&g_srwLock);
...
//當需要寫入資源的時候申請"排他鎖"
AcquireSRWLOckExclusive(&g_srwLock);
//執行寫入動作
...
//寫入結束后釋放"排他鎖"
ReleaseSRWLockExclusive(&g_srwLock);
//-----------------------------------------
//當需要讀取時申請"共享鎖"
AcquireSRWLockShared(&g_srwLock);
//執行讀操作
...
//讀取結束后釋放"共享鎖"
ReleaseSRWLockShared(&g_srwLock);


//系統會自動清理g_srwLock,沒有別的清理函數

可以看到,存在兩種鎖用於此機制:"排他鎖"和"共享鎖"。排他鎖需要任何中鎖都不存的情況下才能返回,否則阻塞;共享鎖在沒有排他鎖的情況下就可以返回,哪怕有其他共享鎖也沒關系。這樣在寫操作之前申請排他鎖,在讀之前申請共享鎖就可以保證共享資源數據不會被意外破壞。另外,只有初始化函數而沒有釋放函數。

看似SRWLock更關鍵段十分相似,但相比關鍵段SRWLock缺乏下面兩個特性:

1、不存在對應的TryEnterXXX函數,如果鎖已被占用那么只能選擇阻塞調用線程;

2、不能遞歸的獲得SRWLock。也就是說一個線程不能為了多次寫入資源而多次鎖定資源。而關鍵段可以做到。回想一下,因為關鍵段在Enter的時候將判斷當前線程是否是共享資源的占有者,如果是則會“放行”,並增加引用計數。然而SRWLock始終不關心調用線程是誰。

 

SRWLock配合條件變量

SRWLock的另一個有用之處就是能配合“條件變量”使用,來完成一些更復雜的同步任務。我們假設如下場景:有一個共享的隊列,2個服務端線程負責讀取隊列中的條目以處理,2個客戶端線程負責寫入隊列中的條目以使服務先端線程處理,當隊列中沒有條目的時候應當掛起服務端線程,直到有條目進入時才被喚醒,另一方面,當隊列已滿時,客戶端線程應當掛起直到服務端至少處理了一個條目,以釋放至少一個條目的空間。

首先創建幾個共享資源鎖,其中SRWLOCK是上文提到的讀寫鎖,CONDITION_VARIABLE就是這里的條件變量:

SRWLOCK  g_srwLock;
CONDITION_VARIABLE g_cvReadyToProduce;//讀取線程用於通知寫入線程可以開始寫入
CONDITION_VARIABLE g_cvReadyToConsume;//寫入線程用於通知讀取線程可以開始讀取

設計如下客戶端寫入線程流程:

image

AcquireSRWLockExclusive(&g_srwLock):在寫入之前獲得排他鎖,如果有其他鎖,則阻塞;

SleepConditionVariableSRW(&g_cvReadToProduce,&g_srwLock,INFINITE,0):當隊列已滿時,等待g_cvReadToProduce變量信號(此信號應該由做讀取操作的服務端線程發起)。參數:&g_srwLock,INFINITE,0 分別表示暫時釋放g_srwLock鎖,並永久等待變量信號;

Write Queue...:對隊列的寫操作,如果在上一步時經過Sleep,並臨時釋放了g_srwLock鎖,在這一步會自動重新獲得g_srwLock鎖;

ReleaseSRWLockExclusive(&g_srwLock):寫完隊列后釋放排他鎖;

WakeAllConditionVariable(&g_cvReadToConsume):向所有正在等待隊列中條目的服務端線程發起g_cvReadToConsume信號,通知他們開始讀取隊列;

 

設計如下的服務端讀取流程:

image

AcquireSRWLockShared(&g_srwLock):在寫入之前獲得共享鎖,如果有排他鎖,則會阻塞;

SleepConditionVariableSRW(&g_cvReadToConsume,&g_srwLock,INFINITE,CONDITION_VARIABLE_LOCKMODE_SHARED):當隊列空時,等待g_cvReadToConsume變量信號(此信號應該由做寫入操作的客戶端線程發起)。參數:&g_srwLock,INFINITE,CONDITION_VARIABLE_LOCKMODE_SHARED 分別表示共享g_srwLock鎖(不釋放),並永久等待變量信號;

Read Queue...:對隊列的讀操作;

ReleaseSRWLockShared(&g_srwLock):讀完隊列后釋放共享鎖;

WakeAllConditionVariable(&g_cvReadToProduce):向所有正在等待隊列至少有一個空間條目的客戶端線程發起g_cvReadToProduce信號,通知他們可以開始寫入隊列;

 

上面的過程中我們對一個場景做了設計用到了SRWLock和所謂的條件變量,總結一下條件變量的用法如下:

CONDITION_VARIABLE g_cvReadyToProduce; //創建變量

VOID WINAPI InitializeConditionVariable(
  __out  PCONDITION_VARIABLE ConditionVariable
);

//等待變量信號
BOOL WINAPI SleepConditionVariableSRW(
  __inout  PCONDITION_VARIABLE ConditionVariable,
  __inout  PSRWLOCK SRWLock,
  __in     DWORD dwMilliseconds,
  __in     ULONG Flags
);

//發出變量信號,以喚醒正在等待變量的線程
VOID WINAPI WakeAllConditionVariable(
  __inout  PCONDITION_VARIABLE ConditionVariable
);

前面一篇講到關鍵段的時候也提到過條件變量,事實上,關鍵段也可以使用條件變量,只是API不太相同:

BOOL WINAPI SleepConditionVariableCS(
  __inout  PCONDITION_VARIABLE ConditionVariable,
  __inout  PCRITICAL_SECTION CriticalSection,
  __in     DWORD dwMilliseconds
);

VOID WINAPI WakeAllConditionVariable(
  __inout  PCONDITION_VARIABLE ConditionVariable
);

另外,除了WakeAllConditionVariable還有一個WakeConditionVariable,顧名思義,后者是單單喚醒一個正在等待變量的線程,這類似於”事件的自動重置”。

關於更多條件變量的信息可以參見:Using Condition Variables

勞動果實,轉載請注明出處:http://www.cnblogs.com/P_Chou/archive/2012/06/24/slim-rw-read-in-thread-sync.html


免責聲明!

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



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