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






























