go 互斥鎖實現原理
go 互斥鎖的實現
go中通過mutex來實現對互斥資源的鎖定
1. mutex的數據結構
1.1 mutex結構體,搶鎖解鎖原理
go type Mutex struce{ state int32 sema uint32 }
- state表示互斥鎖的狀態,比如是否被鎖定
- sema表示訊號量,協程阻塞等待該訊號量來喚醒協程,解鎖的協程釋放該訊號量來喚醒阻塞的協程
下圖展示了mutex的記憶體布局
- Locked:表示mutex是否已經鎖定。1:鎖定。 0:沒有鎖定
- Waiter:表示阻塞等待鎖的協程個數,協程解鎖時根據此值來判斷是否需要釋放訊號量
- Starving:表示該mutex是否處於飢餓狀態。0:沒有飢餓。1:飢餓,表示有協程已經阻塞超過1ms
- Woken:表示是否有協程已被喚醒,0:沒有協程喚醒。1:有協程喚醒,正在加鎖過程中
協程之間搶鎖的過程實際上是給Locked賦值1的過程,能給Locked賦值為1,表示搶鎖成功,搶不到鎖就阻塞等待sema訊號量來喚醒加鎖
1.2 mutex方法
Mutex對外提供兩個方法
- Lock():加鎖方法
- Unlock():解鎖方法
2. 加解鎖過程
2.1 簡單加鎖
假定當前只有一個協程在加鎖,沒有其他協程干擾,加鎖過程如下
加鎖過程會去判斷Locked是否為1,如果是0則把Locked置為1,表示加鎖成功,其他狀態位不會發生變化
2.2 加鎖被阻塞
假定加鎖時,鎖被其他協程佔用,那麼加鎖過程如下:
如果協程對一個已經被佔用的協程加鎖時,Waiter計數器會增加1,此時B將會阻塞,直到Locked變為0後才會喚醒
2.3 簡單解鎖
假定解鎖時,沒有其他協程阻塞,那麼解鎖過程如下
由於此時watier值為0,表示沒有其他協程在等待,所以無須釋放訊號量,只要把Locked置為0即可
2.4 解鎖並釋放協程
假定解鎖過程,有1個或多個協程阻塞,那麼此時的解鎖過程
協程A解鎖分為兩個步驟
- 將Locked置為0
- 釋放sema訊號量,喚醒協程B,並將waiter減1
此時Locked為0,協程B收到訊號量,將Locked置為1,B獲得鎖
3. 自旋過程
加鎖時,如果當前Locked位為1, 說明該鎖被其他協程佔用,但嘗試加鎖的協程並不會馬上轉為阻塞狀態,而是會持續的檢測Locked位是否為0,這個過程稱為自旋。
自旋的過程很短,如果在自旋過程中發現鎖被釋放,那麼該協程會立即獲得鎖,被喚醒的協程會繼續阻塞
自旋的好處是,當加鎖失敗時,不必立即轉入阻塞,有一定機會獲得鎖,避免了協程之間的切換
3.1 什麼是自旋
自旋對應CPU指令”PAUSE”(暫停,停頓),CPU對該指令什麼都不做,相當於CPU空轉,對程式而要相當於sleep了一段時間,該時間非常短,當前為30個時鐘周期
自旋過程會持續檢測Locked是否為0,它不同於sleep,不需要協程轉為睡眠狀態
3.2 自旋條件
- 自旋次數要足夠小,通常為4,即自旋最多4次
- CPU核數要大於1,否則自旋沒有意義,因為此時不可能有其他協程釋放鎖
- 協程調度機制中的Process數量要大於1,比如使用GOMAXPROCS()將處理器設置為1就不能啟用自旋
- 協程調度機制中的可運行隊列必須為空,否則會延遲協程調度
3.3 自旋的優勢
自旋的優勢是更充分的利用了CPU,避免了協程切換。因為當前申請加鎖的協程獲得了CPU,如果通過短時間自旋就可以獲得鎖,那麼就可以直接運行,而不用阻塞並切換協程
3.4 自旋的問題
如果自旋過程中獲得了鎖,那麼之前阻塞的協程就無法獲得鎖。如果等待加鎖的協程特別多,而都在自旋過程中獲得了鎖,那麼之前阻塞的協程就將一直阻塞。
為了解決這個問題,在1.8的版本之後增加了一個狀態Starving。在這個狀態下不會自旋,一定會有一個協程被喚醒並加鎖
4. Mutex模式
現在我們看下Starving位的作用
每個Mutex都有兩個模式,稱為normal和Starving。
4.1 Normal模式
默認情況下都是Normal模式
當一個協程加鎖失敗時,不會立即轉入等待狀態,而是判斷是否滿足自旋條件,如果滿足,則自旋來等待鎖
4.2 Starving模式
自旋模式搶到鎖,表示有協程釋放了鎖,我們知道釋放鎖時,如果waiter>0,即有阻塞等待的協程,會釋放訊號量來喚醒協程,當協程被喚醒後,發現Locked=1,鎖又被搶佔,則又會阻塞,但在阻塞前會判斷自上次阻塞到本次阻塞經歷了多長時間,如果超過1ms的話,會將Mutex標記為”飢餓”模式,然後再阻塞
在飢餓模式下,不會啟動自旋,如果有協程釋放鎖,那麼一定會喚醒一個協程,被喚醒的協程會獲得鎖,同時會把waiter減1.
5. Woken狀態
Woken狀態作用於加鎖和解鎖的過程中,如果一個協程正在解鎖,另一個協程在自旋等待加鎖,那麼會把Woken狀態置為1,通知解鎖的協程不用釋放訊號量。
6. 為什麼重複解鎖要panic
unlock()過程分為將locked置為0,然後判斷waiter是否大於0,如果大於0就釋放訊號量
如果多次unlock(),則可能會喚醒多個協程,多個協程喚醒後會繼續在Lock()的邏輯里搶鎖,勢必會增加Lock()實現的複雜度,也會引起不必要的協程切換。