Go 語(yǔ)言中 sync.Mutex 的實(shí)現(xiàn)
鎖的獲取和釋放模式
先理解兩種不同的鎖的獲取和釋放模式"Barging" 和 "Handoff",它們影響著等待鎖的 goroutines 的行為。
Barging(插隊(duì))
在 Barging 模式下,當(dāng)一個(gè)鎖被釋放時(shí),任何嘗試獲取該鎖的 goroutine 都有機(jī)會(huì)立即搶占("插隊(duì)")并嘗試獲取鎖,而不管是否有其他 goroutines 正在等待。這意味著新到達(dá)的 goroutine 可能會(huì)在等待的 goroutine 之前獲取鎖,即使那些 goroutine 已經(jīng)在等待隊(duì)列中等待了一段時(shí)間。
優(yōu)點(diǎn):是它可以減少喚醒等待線程的延遲,因?yàn)椴恍枰@式地從等待隊(duì)列中選擇一個(gè)線程并喚醒它。這可以提高性能,特別是在鎖的競(jìng)爭(zhēng)不是很激烈的情況下。
缺點(diǎn):是可能導(dǎo)致 "饑餓",即某些 goroutine 可能會(huì)被不斷地插隊(duì),從而無(wú)法及時(shí)獲取鎖。

如上圖,Barging 模式既會(huì)喚醒等待者 G2,G3 作為新到達(dá)的 gorouting 也有機(jī)會(huì)直接獲得鎖
Handoff(交接)
在 Handoff 模式下,當(dāng)一個(gè)鎖被釋放時(shí),持有鎖的 goroutine 顯式地將鎖交給等待隊(duì)列中的下一個(gè) goroutine。這意味著鎖的所有權(quán)從釋放鎖的 goroutine 直接傳遞給等待隊(duì)列中的一個(gè)特定 goroutine,而不允許其他 goroutine 插隊(duì)。
優(yōu)點(diǎn):是它可以防止饑餓,因?yàn)榈却龝r(shí)間最長(zhǎng)的 goroutine 將被優(yōu)先考慮。這種模式確保了公平性,因?yàn)槊總€(gè) goroutine 都會(huì)按照它們到達(dá)等待隊(duì)列的順序獲得服務(wù)。
缺點(diǎn):可能會(huì)引入額外的延遲,因?yàn)樾枰@式地管理等待隊(duì)列,并在每次釋放鎖時(shí)喚醒特定的 goroutine。

如上圖,Handoff 模式喚醒等待隊(duì)列中的 G2,之后直接把鎖交接給 G2,即使喚醒過(guò)程中 G3、G4 正在請(qǐng)求鎖
Go 語(yǔ)言中的實(shí)現(xiàn)
在 Go 語(yǔ)言的 sync.Mutex 實(shí)現(xiàn)中,為了平衡性能和公平性,采用了一種混合的策略。在某些情況下,它允許 Barging,以減少喚醒等待 goroutine 的開(kāi)銷(xiāo);在其他情況下,它使用 Handoff,以確保長(zhǎng)時(shí)間等待的 goroutine 最終能夠獲取鎖,從而防止饑餓。
Go 1.9 引入了一些改進(jìn),使得 sync.Mutex 更加傾向于公平性,通過(guò)引入一個(gè)饑餓狀態(tài),當(dāng)一個(gè) goroutine 等待超過(guò)一定的時(shí)間閾值(1ms)時(shí),它會(huì)進(jìn)入饑餓狀態(tài)。在饑餓狀態(tài)下,鎖的所有權(quán)會(huì)直接從解鎖的 goroutine 交給等待隊(duì)列中的下一個(gè) goroutine,這類(lèi)似于 Handoff 模式。當(dāng)沒(méi)有 goroutine 處于饑餓狀態(tài)時(shí),鎖的獲取更加自由,可能會(huì)出現(xiàn) Barging。
這種混合策略旨在在高并發(fā)場(chǎng)景下提供良好的性能,同時(shí)在鎖競(jìng)爭(zhēng)激烈時(shí)保持足夠的公平性,以避免饑餓問(wèn)題。
自旋
自旋(spinning)是一種優(yōu)化技術(shù),用于在某些情況下避免 goroutine 在等待鎖時(shí)立即進(jìn)入休眠狀態(tài)。自旋可以讓 goroutine 在短時(shí)間內(nèi)忙等(busy-wait),以期在這段時(shí)間內(nèi)鎖被釋放,從而避免了系統(tǒng)調(diào)用的開(kāi)銷(xiāo)和上下文切換的成本。
自旋在 Go 的 sync.Mutex 中是這樣使用的:
自旋嘗試:當(dāng)一個(gè) goroutine 嘗試獲取一個(gè)已經(jīng)被其他 goroutine 持有的鎖時(shí),它會(huì)執(zhí)行一個(gè)有限次數(shù)的自旋嘗試。在這個(gè)過(guò)程中,goroutine 會(huì)在用戶空間中忙等,檢查鎖是否已經(jīng)被釋放。
退讓:如果在自旋嘗試期間鎖沒(méi)有被釋放,goroutine 可能會(huì)調(diào)用 runtime.Gosched() 或類(lèi)似的函數(shù)來(lái)讓出 CPU 時(shí)間片,給其他 goroutine 執(zhí)行的機(jī)會(huì)。這樣做可以減少 CPU 的無(wú)效消耗,尤其是在單核心或者核心數(shù)較少的情況下。
阻塞:如果自旋后鎖仍然不可用,goroutine 最終會(huì)停止自旋,并通過(guò)更重的同步機(jī)制(如 futex 或其他內(nèi)核同步原語(yǔ))進(jìn)入休眠狀態(tài),等待被喚醒。

自旋與 Barging 和 Handoff 模式的關(guān)系
與 Barging 的關(guān)系:自旋與 Barging 模式結(jié)合得很好,因?yàn)樵?Barging 模式下,鎖一旦被釋放,任何 goroutine 都有機(jī)會(huì)獲取它。因此,自旋的 goroutine 可能會(huì)在鎖釋放時(shí)立即獲取到鎖,而不需要被喚醒。
與 Handoff 的關(guān)系:在 Handoff 模式下,鎖的所有權(quán)直接從釋放鎖的 goroutine 傳遞給等待隊(duì)列中的下一個(gè) goroutine。在這種情況下,自旋可能不那么有效,因?yàn)殒i的獲取是預(yù)定的,不會(huì)立即發(fā)生。但是,如果等待隊(duì)列中沒(méi)有饑餓的 goroutine,那么自旋的 goroutine 仍然有機(jī)會(huì)在鎖釋放時(shí)立即獲取它。
Go 語(yǔ)言的運(yùn)行時(shí)會(huì)根據(jù)當(dāng)前的情況(如鎖的競(jìng)爭(zhēng)程度和處理器的數(shù)量)來(lái)決定是否使用自旋,以及自旋的次數(shù)。這是一個(gè)經(jīng)過(guò)調(diào)優(yōu)的決策,目的是在減少延遲和避免浪費(fèi) CPU 資源之間找到平衡點(diǎn)。
sync_runtime_canSpin 判斷是否可以進(jìn)入自旋
// Active spinning for sync.Mutex. //go:linkname sync_runtime_canSpin sync.runtime_canSpin //go:nosplit func sync_runtime_canSpin(i int) bool { // sync.Mutex is cooperative, so we are conservative with spinning. // Spin only few times and only if running on a multicore machine and // GOMAXPROCS>1 and there is at least one other running P and local runq is empty. // As opposed to runtime mutex we don't do passive spinning here, // because there can be work on global runq or on other Ps. if i >= active_spin || ncpu <= 1 || gomaxprocs <= int32(sched.npidle+sched.nmspinning)+1 { return false } if p := getg().m.p.ptr(); !runqempty(p) { return false } return true }
如果可以自選,就通過(guò) sync_runtime_doSpin 進(jìn)入自旋,對(duì)應(yīng)執(zhí)行 30 次 PAUSE 指令
PAUSE 指令會(huì)告訴 CPU 我當(dāng)前處于處于自旋狀態(tài),這時(shí)候 CPU 會(huì)針對(duì)性的做一些優(yōu)化,并且在執(zhí)行這個(gè)指令的時(shí)候 CPU 會(huì)降低自己的功耗,減少能源消耗
//go:linkname sync_runtime_doSpin sync.runtime_doSpin //go:nosplit func sync_runtime_doSpin() { procyield(active_spin_cnt) } TEXT runtime·procyield(SB),NOSPLIT,$0-0 MOVL cycles+0(FP), AX again: PAUSE SUBL $1, AX JNZ again RET
Mutex 源碼結(jié)構(gòu)
// A Mutex is a mutual exclusion lock. // The zero value for a Mutex is an unlocked mutex. // // A Mutex must not be copied after first use. type Mutex struct { state int32 sema uint32 } mutexLocked = 1 << iota // mutex is locked mutexWoken mutexStarving mutexWaiterShift = iota
state:4 字節(jié)的 Int32 類(lèi)型,最低的 3 個(gè) bit 表示鎖的狀態(tài),分別是 mutexLocked mutexWoken mutexStarving ,剩下的 bit 用于統(tǒng)計(jì)當(dāng)前在等待鎖的 goroutine 數(shù)量
sema:控制 gorouting 在獲取鎖過(guò)程當(dāng)中的休眠和喚醒,在 Linux 系統(tǒng)中,信號(hào)量可以通過(guò)多種方式實(shí)現(xiàn),其中之一是使用 "futex"(快速用戶空間互斥鎖)。futex 是一種系統(tǒng)調(diào)用,它提供了一種在用戶空間線程之間進(jìn)行高效等待和喚醒的機(jī)制
Lock 方法

Fast Path:首先嘗試一個(gè)快速路徑,如果鎖是空閑的(state 為 0),就通過(guò)原子操作 CompareAndSwapInt32 嘗試將其設(shè)置為 mutexLocked 狀態(tài)。如果成功,就直接返回,表示獲取鎖成功。
Slow Path:如果快速路徑失敗,表示鎖已經(jīng)被其他 goroutine 持有,那么就進(jìn)入慢路徑。在慢路徑中,會(huì)有一個(gè)循環(huán),goroutine 會(huì)在這里等待,直到它能夠獲取鎖。
Starvation Detection: 代碼中有對(duì)饑餓狀態(tài)的檢測(cè),如果一個(gè) goroutine 長(zhǎng)時(shí)間等待鎖,它可能會(huì)進(jìn)入饑餓模式,這時(shí)它會(huì)被優(yōu)先喚醒。
Waiters Counting: 如果鎖已經(jīng)被持有,goroutine 會(huì)增加等待者的計(jì)數(shù),并可能進(jìn)入休眠狀態(tài),等待鎖被釋放。
Waking Up: 當(dāng)鎖被釋放時(shí),等待的 goroutine 會(huì)被喚醒。如果有多個(gè)等待者,喚醒操作會(huì)嘗試保持公平性,避免某些 goroutine 饑餓。
Unlock 方法

Fast Path: 通過(guò)原子操作減少 state 的值來(lái)釋放鎖。如果減少后的值表明鎖已經(jīng)是未鎖定狀態(tài),那么會(huì)拋出 panic,因?yàn)檫@意味著試圖釋放一個(gè)未鎖定的互斥鎖。
No Waiters: 如果沒(méi)有等待者,或者已經(jīng)有一個(gè) goroutine 被喚醒或處于饑餓狀態(tài),那么就沒(méi)有必要進(jìn)行喚醒操作。
Wake Someone Up: 如果有等待者,那么會(huì)釋放一個(gè)信號(hào)量 sema 來(lái)喚醒一個(gè)等待的 goroutine。
參考:
1. https://lailin.xyz/post/go-training-week3-sync.html#RWMutex

浙公網(wǎng)安備 33010602011771號(hào)