golang mutex 兩加兩解助你實現高併發控制

天下之事常成於困約,而敗於奢靡。——陸游

1 前言

互斥鎖是併發程序中對共享資源進行訪問控制的主要手段,因此 Go 設計者提供了非常簡單易用的 Mutex 供我們使用,接下來我們從源碼剖析實現原理,又不會過分糾結於實現細節。

2 Mutex 數據結構

2.1 結構體定義

type Mutex struct {
    state int32
    sema  uint32
}

state 是 32 位的整型變量,內部實現時把該變量分成四份,用於記錄 Mutex 的四種狀態, 如下圖:

協程之間搶鎖實際上是搶給 Locked 賦值的權利,能給 Locked 字段置爲 1,就說明搶鎖成功。搶不到的話就阻塞等待 sema 信號量,一旦持有鎖的協程解鎖,等待的協程會依次被喚醒。

Woken 和 Starving 主要用於控制協程間的搶鎖過程。

2.2 方法

Mutext 對外提供兩個方法:

下面我們分析一下加鎖和解鎖的過程,加鎖分成功和失敗兩種情況,成功的話直接獲取鎖,失敗後當前協程被阻塞,同樣,解鎖時跟據是否有阻塞協程也有兩種處理。

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