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解鎖分為兩個步驟

  1. 將Locked置為0
  2. 釋放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()實現的複雜度,也會引起不必要的協程切換。