golang mutex 兩加兩解助你實現高併發控制
天下之事常成於困約,而敗於奢靡。——陸游
1 前言
互斥鎖是併發程序中對共享資源進行訪問控制的主要手段,因此 Go 設計者提供了非常簡單易用的 Mutex 供我們使用,接下來我們從源碼剖析實現原理,又不會過分糾結於實現細節。
2 Mutex 數據結構
2.1 結構體定義
type Mutex struct {
state int32
sema uint32
}
state 是 32 位的整型變量,內部實現時把該變量分成四份,用於記錄 Mutex 的四種狀態, 如下圖:
-
Locked: 表示該 Mutex 是否已被鎖定,0:沒有鎖定 1:已被鎖定。
-
Woken: 表示是否有協程已被喚醒,0:沒有協程喚醒 1:已有協程喚醒,正在加鎖過程中。
-
Starving:表示該 Mutex 是否處於飢餓狀態, 0:沒有飢餓 1:飢餓狀態,說明有協程阻塞了超過 1ms。
-
Waiter: 表示阻塞等待鎖的協程個數,協程解鎖時根據此值來判斷是否需要釋放信號量。
協程之間搶鎖實際上是搶給 Locked 賦值的權利,能給 Locked 字段置爲 1,就說明搶鎖成功。搶不到的話就阻塞等待 sema 信號量,一旦持有鎖的協程解鎖,等待的協程會依次被喚醒。
Woken 和 Starving 主要用於控制協程間的搶鎖過程。
2.2 方法
Mutext 對外提供兩個方法:
-
Lock() : 加鎖方法
-
Unlock(): 解鎖方法
下面我們分析一下加鎖和解鎖的過程,加鎖分成功和失敗兩種情況,成功的話直接獲取鎖,失敗後當前協程被阻塞,同樣,解鎖時跟據是否有阻塞協程也有兩種處理。
3 加鎖
3.1 簡單加鎖
假如當前只有一個 goroutine 在加鎖,沒有其他 goroutine 干擾,那麼加鎖過程如下圖:
加鎖過程會去判斷 Locked 標誌位是否爲 0,如果是 0 則把 Locked 位置 1,代表加鎖成功。從上圖可見,加鎖成功後,只是 Locked 位置 1,其他狀態位沒發生變化。
3.2 加鎖被阻塞
假如加鎖時,鎖已經被其他 goroutine 佔用了,此時加鎖過程如下圖:
從上圖可看到,當 goroutine-B 對一個已被佔用的鎖再次加鎖時,Waiter 計數器增加 1,此時 goroutine-B 將被阻塞,直到 Locked 值變爲 0 後纔會被喚醒。
4 解鎖
4.1 簡單解鎖
假如解鎖時,沒有其他 goroutine 阻塞,此時解鎖過程如下圖:
由於沒有其他 goroutine 阻塞等待加鎖,所以此時解鎖時只需要把 Locked 位置爲 0 即可,不需要釋放信號量。
4.2 解鎖並喚醒協程
假如解鎖時,有 1 個或多個 goroutine 阻塞,此時解鎖過程如下圖:
goroutine-A 解鎖過程分爲兩個步驟,一是把 Locked 位置爲 0,二是查看到 Waiter>0,所以釋放一個信號量,喚醒一個阻塞的 goroutine,被喚醒的 goroutine-B 把 Locked 位置爲 1,於是 goroutine-B 獲得鎖。
5 小結
目前沒有涉及到大量源碼,因爲 mutex 的源碼本身比較多,由於篇幅關係這裏只是提取了 mutex 的實現原理,但是還有部分細節準備在下次分享中繼續討論,比如自旋,工作模式以及飢餓模式等。
6 關注公衆號
微信公衆號:堆棧 future
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/b4Hd6dcMMFD-JGQaks3S_w