Go 語言并發編程互斥鎖 sync.Mutex 底層實現
1.介紹
本文通過閱讀 Go 語言 sync.Mutex 的源碼,我們一起學習 sync.Mutex 的底層實現。
2.sync.Mutex` 源碼[1]分析
我們通過閱讀 Go 語言 sync.Mutex 的源碼,可以發現 sync.Mutex 結構體包含兩個字段:
type Mutex struct {
state int32
sema uint32
}
- state:存儲互斥鎖的狀態信息,包括鎖是否被占用、是否進入饑餓模式,以及是否有協程被喚醒等。
- sema:信號量,用于阻塞和喚醒等待該互斥鎖的協程。
state 字段
state 是一個 int32 類型的整數,用來表示互斥鎖的當前狀態。它并不是一個簡單的布爾值,而是通過多個位(bit)來記錄鎖的不同狀態。通過位運算,可以在同一個字段中存儲多種狀態信息。我們通過閱讀 lockSlow() 方法的源碼,可以發現 state 包含的幾種狀態。
state 主要包含以下幾種狀態:
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift
starvationThresholdNs = 1e6
)
- mutexLocker:表示互斥鎖是否已被鎖定。mutexLocked 是通過位移操作 1 << iota 定義的,當其值為 1 時,表示互斥鎖已被鎖定。
- mutexWoken:表示是否有等待的協程已被喚醒。這個狀態位防止多個等待的協程被同時喚醒,從而避免競爭鎖。
- mutexStarving:表示互斥鎖是否處于饑餓模式。當一個等待的協程長時間無法獲得鎖(超過 1 毫秒),互斥鎖會進入饑餓模式,此時鎖的所有權會直接從釋放鎖的協程傳遞給等待隊列中的下一個協程。
- mutexWaiterShift:表示有多少協程在等待獲取互斥鎖。通過將 state 字段右移來記錄當前有多少協程處于等待狀態(每個等待的協程增加一個值)。
推薦讀者朋友們在項目開發中,多嘗試使用位運算。
此外,我們通過閱讀 lockSlow() 方法的源碼,發現其內部實現中,使用“自旋鎖”和“CAS”,分別是 runtime_doSpin() 和 atomic.CompareAndSwapInt32()。
使用“自旋鎖”,當互斥鎖可能很快被釋放時,協程可能會短暫地自旋等待,從而減少 CPU 上下文切換的開銷。
需要注意的是“自旋鎖”會占用 CPU 資源,我們在項目開發中使用時,切勿長時間進行自旋等待。
使用“CAS”,用于對 state 變量進行原子更新,確保線程安全。
sema 字段
sema 是一個 uint32 類型的信號量,用來控制阻塞和喚醒等待互斥鎖的協程。它通過與操作系統底層機制交互,負責在鎖被占用時阻塞協程,當鎖被釋放時喚醒等待中的協程。
當一個協程嘗試獲取鎖但鎖已被占用時,它需要進入阻塞狀態。 sema 字段與 Go 的 runtime(運行時)機制合作,將這些等待的協程掛起。當鎖被釋放時,runtime 會通過信號量來喚醒一個或多個等待的協程。
我們閱讀 lockSlow() 方法和 unlockSlow() 方法的源碼,可以發現 sema 通過 runtime_SemacquireMutex() 和 runtime_Semrelease() 函數進行操作。runtime_SemacquireMutex() 阻塞當前協程并等待信號量,runtime_Semrelease() 則負責釋放信號量并喚醒等待的協程。
通過使用信號量,可以很好地處理高并發下的協程調度問題。與自旋鎖不同,信號量機制不會占用 CPU 資源。當協程需要等待鎖時,它可以通過信號量進入休眠,等待鎖釋放后再被喚醒,避免了忙等待帶來的性能損耗。
3.總結
本文我們通過閱讀 Go 語言 sync.Mutex 的源碼,更加深入了解 sync.Mutex 的底層實現,它包含兩種操作模式,分別是:
普通模式:
在普通模式下,等待的協程按 FIFO 順序排隊,但新到達的協程可以和被喚醒的協程競爭鎖的所有權,因為新協程已經在 CPU 上運行,有一定的優勢(即可以減少 CPU 上下文切換,從而提升性能)。如果一個協程等待超過 1 毫秒,互斥鎖會切換到饑餓模式。
饑餓模式:
當協程等待超過 1 毫秒時,互斥鎖進入饑餓模式。在饑餓模式下,新到達的協程不再直接嘗試獲取鎖,而是排隊等待。釋放鎖的協程會將鎖直接交給隊列中的第一個等待協程,從而避免長期等待的協程一直得不到鎖的情況。
state 字段通過位操作存儲了互斥鎖的多種狀態,包括是否鎖定、是否進入饑餓模式、等待隊列長度等,允許通過原子操作對這些狀態進行高效的并發管理。
sema 字段是一個信號量,用于阻塞和喚醒等待鎖的協程,結合 Go runtime 的機制,實現高效的協程調度和喚醒。
這兩個字段共同構成了 sync.Mutex 的核心,保證了在高并發場景下的互斥鎖操作既高效又安全。
隨著 Go 語言版本迭代,sync.Mutex 的實現經過高度優化,能夠在低競爭和高競爭場景中提供高效的鎖定機制,同時盡量減少協程“饑餓”的情況。