用户模式下的线程同步
在以下兩種基本情況下,線程之間需要相互通信
1.需要讓多個線程同時訪問一個共享資源,同時不能破壞資源的完整性
2.一個線程需要通知其他線程某項任務已經完成。
原子訪問相關的內容就直接略過了,因為感覺實際使用的過程中并不多。
下面直接開始說一下關鍵段,它在執行之前需要獨占對一些共享資源的訪問權。這種方式可以讓多行代碼以原子方式來對資源進行操控。這里的原子方式,指的是代碼知道除了當前線程之外,沒有其他任何線程會同時訪問該資源。在當前線程離開關鍵段之前,系統是不會去調度任何想要訪問同一資源的其他線程的。
一般是EnterCriticalSection和LeaveCriticalScetion配對使用,需要先創建一個CRITICAL_SECTION結構。
這結構一般都是作為全局變量來使用,也可以作為局部變量來分配或者堆中,另外作為類的一個私有字段還分配也是很常見的。
在使用CRITICAL_SECTION的時候,只有兩個必要條件,第一個是所有想要訪問資源的線程必須知道用來保護資源的CRITICAL_SECTION結構的地址,第二個是任何線程試圖訪問被保護的資源之前,必須對CRITICAL_SECTION結構的內部成員進行初始化。
下面的函數用來進行初始化
void InitializerCriticalSection(PCRITICAL_SECTION pcs);
當線程不在需要訪問共享資源的時候,應該調用下面的函數來清理結構
void deleteCriticalSection(PCRITICAL_SECTION pcs);
EnterCriticalSection會檢查結構體中的成員變量,這些變量表示是否有線程正在訪問資源,以及哪個線程正在訪問資源。EnterCriticalSection會執行下面的測試:
1.如果沒有線程正在訪問資源,那么EnterCriticalSection會更新成員變量,以表示調用線程已經獲準對資源的訪問,并立即訪問,這樣線程就可以繼續執行
2.如果成員變量表示調用線程已經獲準訪問資源,那么EnterCriticalSection會更新變量,以表示調用線程被獲準訪問的次數,并立即放回,這樣線程就可以繼續執行。這樣的情況非常少見,只有當線程調用LeaveCriticalSection之前連續調用EnterCriticalSection兩次以上才會發生。
3.如果成員變量表示有一個(調用線程之外的其他)線程已經獲準訪問資源,那么EnterCriticalSection會使用一個事件內核對象來吧線程切換到等待狀態。一旦當前正在訪問資源的線程調用了LeaveCriticalSection,系統會自動更新結構的成員變量并將等待中的線程切換回可調度狀態。
我們也可以用下面的函數來代替EnterCriticalSection
Bool TryEnterCriticalSection(PCRITICAL_SECTION pcs);
TryEnterCriticalSection從來不會讓調用線程進入等待狀態,他會通過返回值來表示調用線程是否獲準訪問資源,因此如果TryEnterCriticalSection發現資源正在被其他線程訪問,那么它會返回false。每一個返回true的TryEnterCriticalSection調用必須有一個對應的LeaveCriticalSection。
當線程試圖進入一個關鍵段,但這關鍵段正在被另一個線程占用的時候,函數會立即把調用線程切換到等待狀態。這意味著線程必須從用戶模式切換到內核模式,這個切換的開銷非常大。為了提高性能,可以將旋轉鎖合并到關鍵段中,因此當調用EnterCriticalSection的時候,他會用一個旋轉鎖不斷的循環,嘗試在一段時間內獲取的對資源的訪問權,只有當嘗試失敗的時候買線程才會切換到內核狀態并進到等待狀態。
為了在是因關鍵段的時候同時使用旋轉鎖,必須調用以下的函數來初始化關鍵段
Bool InitializeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs,DWORD dwSpinCount);
與InitializerCriticalSection相似,但是第二個參數是我們希望旋轉鎖循環的次數,我們也可以通過下面的函數來改變關鍵段的旋轉次數
DWORD WINAPI SetCriticalSectionSpinCount(_Inout_?LPCRITICAL_SECTION lpCriticalSection,_In_????DWORD ?????????????dwSpinCount );用來保護進程對的關鍵段所使用的旋轉次數大約是4000,這里作為參考。
SRWLock的目的和關鍵段系統,對一個資源進行保護,不讓其他資源訪問他。但是與關鍵段不同,SRWLock允許我們區分哪些想要讀取資源的值的線程(讀取者線程)和想要更新資源的值的線程(寫入者線程)。讓所有的讀取者線程在同一時刻訪問共享資源應該是可行的,只有當寫入者線程需要對資源進行更新的時候才需要同步,在這樣子情況下,寫入者線程應該獨占對資源的訪問權:任何其他線程。
首先我們需要分配一個SRWLOCK結構并用InitialSRWLock函數來對他進行初始化
VOID WINAPI InitializeSRWLock(?_Out_? PSRWLOCK SRWLock? );?
一旦SRWLock的初始化完成后,寫入者線程就可以調用AcquireSRWLockExclusive,將SRWLOCK對象的地址作為參數傳入,以嘗試獲得對被保護資源的獨占訪問權。
VOID WINAPI AcquireSRWLockExclusive(? _Inout_? PSRWLOCK SRWLock? );?
在完成對資源的更新之后。應該調用ReleaseSRWLockExclusive函數,并將SRWLOCK對象的地址作為參數傳入,解除對資源的鎖定。
VOID WINAPI ReleaseSRWLockExclusive(?? _Inout_? PSRWLOCK SRWLock? );?
讀取者的操作
讀取者調用的兩個參數是:
VOID AcquireSRWLockShared(PSRWLOCK SRWLock);??
VOID ReleaseSRWLockShared(PSRWLOCK SRWLock);??
不存在刪除或銷毀SRWLock的函數,系統會自動執行清理工作。
與關鍵段相比,SRWLock缺乏下面兩個特性:
(1)不存在TryEnter(Shared/Exclusive)SRWLock之類的函數。如果鎖已經被占用,那么調用AcquireSRWLock(SHared/Exclusive)會阻塞調用線程。
(2)不能遞歸調用SRWLOCK。一個線程不能為了多次寫入資源而多次鎖定資源,然后多次調用ReleaseSRWLock來釋放對資源的鎖定。
但是,如果可以接受這些限制,就可以用SRWLock來代替關鍵段,并獲得實際性能和可伸縮性的提升。
三、同步機制性能的比較
通過一個簡單的基準測試可以比較各種同步機制的性能:產生1、2和4個線程,使用不同的同步機制重復執行相同的任務,在雙處理器上運行,得出的結果是:
用戶模式下同步機制性能的對比實驗結果如下(計數單位:微秒)
| 線程數 | Volatile Read | Volatile Write | Interlocked Increment | Critical Section | SRWLock Shared | SRWLock Exclusive | Mutex |
| 1 | 8 | 8 | 35 | 66 | 66 | 67 | 1060 |
| 2 | 8 | 76 | 153 | 268 | 134 | 148 | 11082 |
| 4 | 9 | 145 | 361 | 768 | 244 | 307 | 23785 |
各種機制對比:
(1)讀取volatile長整型值,讀取非常快,因為不需要進行任何同步,與CPU的高速緩存完全無關。
(2)寫入volatile長整型值。單線程的時間和讀取差不多,但是雙線程的時候時間不只是加倍,這是因為CPU之間必須相互通信以維護高速緩存的一致性。如果機器有更多的CPU,那么性能還會下降,因為需要在更多的CPU之間進行通信來使得所有CPU的高速緩存一致。
(3)使用InterlockedIncrement來安全遞增一個volatile長整型值。
它比第一種方法要慢,這是因為CPU必須鎖定內存。使用兩個線程要比一個線程慢得多,這是因為必須在兩個CPU之間來回傳輸數據以維護高速緩存的一致性。
(4)使用關鍵段來讀取一個volatile長整型值。
關鍵段比較慢,是因為我們必須先進入再離開。進入和離開需要修改CRITICAL_SECTION結果中的多個字段。4個線程需要花費更多時間,是因為上下文切換增大了發生爭奪現象的可能性。
(5)使用SRWLock來讀取一個volatile長整型值。
當有多個線程的時候,讀操作比寫操作快。由于多個線程會不斷地寫入鎖的字段以及它保護的數據,因此各CPU必須在它們的高速緩存之間來回傳輸數據。
它的性能和關鍵段差不多,但是很多時候要優于關鍵段。建議的做法是用SRWLock替代關鍵段。
(6)使用同步內核對象互斥量。
互斥量是目前性能最差的,是因為等待互斥量以及后來釋放互斥量需要線程每次在用戶模式和內核模式之間卻換,開銷很大。
總結:應該首先嘗試不要共享數據,然后依次使用volatile讀取、volatile寫入、Interlocked函數、SRWLock以及關鍵段。僅當這些都不能滿足要求的時候,再使用內核對象。
Windows提供SleepConditionVariableCS或SleepConditionVariableSRW函數,等待條件變量。線程在等待該條件變量時,會以原子方式把鎖釋放并將自己阻塞,直到該條件變量被觸發時為止。
Bool?SleepConditionVariableCS(??PCONDITION_VARIABLE?pConditionVariable,??PCRITICAL_SECTION?pCriticalSection,??DWORD?dwMilliseconds);??Bool?SleepConditionVariableSRW(??PCONDITION_VARIABLE?pConditionVariable,??PSRWLOCK?pSRWLock,??DWORD?dwMilliseconds??ULONG?Flags);?pConditonVariable指向一個以初始化的條件變量,調用線程將等待該條件變量。第二個參數指向一個關鍵段或是SRWLock對象。該關鍵段或SRWLock用來同步對共享資源的訪問。Flags指定一旦條件變量被觸發,線程將以何種方式獲得鎖。對讀取者線程來說應該傳入CONDITION_VARIABLE_LOCKMODE_SHARED表示希望共享對資源的訪問。對于寫入者線程應該傳入0,表示獨占資源。
dwMilliseconds表示我們希望線程花多少時間來等待條件被觸發。在指定的時間用完時,如果條件變量尚未被觸發,函數返回false,否則為true。
當另一個線程檢測到相應的條件已經滿足時,比如存在一個元素可以讓讀取者線程讀取。它會調用WakeConditionVariable或WakeAllConditionVariable,觸發條件變量。這樣調用Sleep*函數而阻塞在該條件變量的線程就會被喚醒。
總結
以上是生活随笔為你收集整理的用户模式下的线程同步的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#异步
- 下一篇: libjpeg(2)