Golang?Mutex?原理詳細(xì)解析
前言
互斥鎖是在并發(fā)程序中對(duì)共享資源進(jìn)行訪問(wèn)控制的主要手段。對(duì)此 Go 語(yǔ)言提供了簡(jiǎn)單易用的 Mutex
。Mutex 和 Goroutine 合作緊密,概念容易混淆,一定注意要區(qū)分各自的概念。
Mutex
是一個(gè)結(jié)構(gòu)體,對(duì)外提供 Lock()
和Unlock()
兩個(gè)方法,分別用來(lái)加鎖和解鎖。
// A Locker represents an object that can be locked and unlocked. type Locker interface { Lock() Unlock() } type Mutex struct { state int32 sema uint32 } const ( mutexLocked = 1 << iota // mutex is locked mutexWoken mutexStarving mutexWaiterShift = iota )
- Mutex 是一個(gè)互斥鎖,其零值對(duì)應(yīng)了未上鎖的狀態(tài),不能被拷貝;
- state 代表互斥鎖的狀態(tài),比如是否被鎖定;
- sema 表示信號(hào)量,協(xié)程阻塞會(huì)等待該信號(hào)量,解鎖的協(xié)程釋放信號(hào)量從而喚醒等待信號(hào)量的協(xié)程。
注意到 state 是一個(gè) int32 變量,內(nèi)部實(shí)現(xiàn)時(shí)把該變量分成四份,用于記錄 Mutex 的狀態(tài)。
- Locked: 表示該 Mutex 是否已經(jīng)被鎖定,0表示沒(méi)有鎖定,1表示已經(jīng)被鎖定;
- Woken: 表示是否有協(xié)程已經(jīng)被喚醒,0表示沒(méi)有協(xié)程喚醒,1表示已經(jīng)有協(xié)程喚醒,正在加鎖過(guò)程中;
- Starving: 表示該 Mutex 是否處于饑餓狀態(tài),0表示沒(méi)有饑餓,1表示饑餓狀態(tài),說(shuō)明有協(xié)程阻塞了超過(guò)1ms;
上面三個(gè)表示了 Mutex 的三個(gè)狀態(tài):鎖定 - 喚醒 - 饑餓。
Waiter 信息雖然也存在 state 中,其實(shí)并不代表狀態(tài)。它表示阻塞等待鎖的協(xié)程個(gè)數(shù),協(xié)程解鎖時(shí)根據(jù)此值來(lái)判斷是否需要釋放信號(hào)量。
協(xié)程之間的搶鎖,實(shí)際上爭(zhēng)搶給Locked
賦值的權(quán)利,能給 Locked
置為1,就說(shuō)明搶鎖成功。搶不到就阻塞等待 sema
信號(hào)量,一旦持有鎖的協(xié)程解鎖,那么等待的協(xié)程會(huì)依次被喚醒。
Woken
和 Starving
主要用于控制協(xié)程間的搶鎖過(guò)程。
Lock
func (m *Mutex) Lock() { // Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // Slow path (outlined so that the fast path can be inlined) m.lockSlow() }
若當(dāng)前鎖已經(jīng)被使用,請(qǐng)求 Lock() 的 goroutine 會(huì)阻塞,直到鎖可用為止。
單協(xié)程加鎖
若只有一個(gè)協(xié)程加鎖,無(wú)其他協(xié)程干擾,在加鎖過(guò)程中會(huì)判斷 Locked
標(biāo)志位是否為 0,若當(dāng)前為 0 則置為 1,代表加鎖成功。這里本質(zhì)是一個(gè) CAS 操作,依賴(lài)了 atomic.CompareAndSwapInt32
。
加鎖被阻塞
假設(shè)協(xié)程B在嘗試加鎖前,已經(jīng)有一個(gè)協(xié)程A獲取到了鎖,此時(shí)的狀態(tài)為:
此時(shí)協(xié)程B嘗試加鎖,被阻塞,Mutex 的狀態(tài)為:
Waiter 計(jì)數(shù)器增加了1,協(xié)程B將會(huì)持續(xù)阻塞,直到 Locked
值變成0 后才會(huì)被喚醒。
Unlock
func (m *Mutex) Unlock() { if race.Enabled { _ = m.state race.Release(unsafe.Pointer(m)) } // Fast path: drop lock bit. new := atomic.AddInt32(&m.state, -mutexLocked) if new != 0 { // Outlined slow path to allow inlining the fast path. // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock. m.unlockSlow(new) } }
如果 Mutex 沒(méi)有被加鎖,就直接 Unlock
,會(huì)拋出一個(gè) runtime error。
從源碼注釋來(lái)看,一個(gè) Mutex 并不會(huì)與某個(gè)特定的 goroutine 綁定,理論上講用一個(gè) goroutine 加鎖,另一個(gè) goroutine 解鎖也是允許的,不過(guò)為了代碼可維護(hù)性,一般還是建議不要這么搞。
A locked Mutex is not associated with a particular goroutine. It is allowed for one goroutine to lock a Mutex and then arrange for another goroutine to unlock it.
無(wú)協(xié)程阻塞下的解鎖
假定在解鎖時(shí),沒(méi)有其他協(xié)程阻塞等待加鎖,那么只需要將 Locked
置為 0 即可,不需要釋放信號(hào)量。
解鎖并喚醒協(xié)程
假定解鎖時(shí)有1個(gè)或多個(gè)協(xié)程阻塞,解鎖過(guò)程分為兩個(gè)步驟:
- 將
Locked
位置0; - 看到
Waiter
> 0,釋放一個(gè)信號(hào)量,喚醒一個(gè)阻塞的協(xié)程,被喚醒的協(xié)程把Locked
置為1,獲取到鎖。
自旋
加鎖時(shí),如果當(dāng)前 Locked
位為1,則說(shuō)明當(dāng)前該鎖由其他協(xié)程持有,嘗試加鎖的協(xié)程并不是馬上轉(zhuǎn)入阻塞,而是會(huì)持續(xù)探測(cè) Locked
位是否變?yōu)?,這個(gè)過(guò)程就是「自旋」。
自旋的時(shí)間很短,如果在自旋過(guò)程中發(fā)現(xiàn)鎖已經(jīng)被釋放,那么協(xié)程可以立即獲取鎖。此時(shí)即便有協(xié)程被喚醒,也無(wú)法獲取鎖,只能再次阻塞。
自旋的好處是,當(dāng)加鎖失敗時(shí)不必立即轉(zhuǎn)入阻塞,有一定機(jī)會(huì)獲取到鎖,這樣可以避免一部分協(xié)程的切換。
什么是自旋
自旋對(duì)應(yīng)于 CPU 的 PAUSE
指令,CPU 對(duì)該指令什么都不做,相當(dāng)于空轉(zhuǎn)。對(duì)程序而言相當(dāng)于sleep
了很小一段時(shí)間,大概 30個(gè)時(shí)鐘周期。連續(xù)兩次探測(cè)Locked
位的間隔就是在執(zhí)行這些 PAUSE
指令,它不同于sleep
,不需要將協(xié)程轉(zhuǎn)為睡眠態(tài)。
自旋條件
加鎖時(shí) Golang 的 runtime 會(huì)自動(dòng)判斷是否可以自旋,無(wú)限制的自旋將給 CPU 帶來(lái)巨大壓力,自旋必須滿足以下所有條件:
- 自旋次數(shù)要足夠少,通常為 4,即自旋最多 4 次;
- CPU 核數(shù)要大于 1,否則自旋沒(méi)有意義,因?yàn)榇藭r(shí)不可能有其他協(xié)程釋放鎖;
- 協(xié)程調(diào)度機(jī)制中的 P 的數(shù)量要大于 1,比如使用
GOMAXPROCS()
將處理器設(shè)置為 1 就不能啟用自旋; - 協(xié)程調(diào)度機(jī)制中的可運(yùn)行隊(duì)列必須為空,否則會(huì)延遲協(xié)程調(diào)度。
可見(jiàn)自旋的條件是很苛刻的,簡(jiǎn)單說(shuō)就是不忙的時(shí)候才會(huì)啟用自旋。
自旋的優(yōu)勢(shì)
自旋的優(yōu)勢(shì)是更充分地利用 CPU,盡量避免協(xié)程切換。因?yàn)楫?dāng)前申請(qǐng)加鎖的協(xié)程擁有 CPU,如果經(jīng)過(guò)短時(shí)間的自旋可以獲得鎖,則當(dāng)前寫(xiě)成可以繼續(xù)運(yùn)行,不必進(jìn)入阻塞狀態(tài)。
自旋的問(wèn)題
如果在自旋過(guò)程中獲得鎖,那么之前被阻塞的協(xié)程就無(wú)法獲得。如果加鎖的協(xié)程特別多,每次都通過(guò)自旋獲取鎖,則之前被阻塞的協(xié)程將很難獲取鎖,從而進(jìn)入【饑餓狀態(tài)】。
為此,Golang 1.8 版本后為Mutex
增加了Starving
模式,在這個(gè)狀態(tài)下不會(huì)自旋,一旦有協(xié)程釋放鎖。那么一定會(huì)喚醒一個(gè)協(xié)程并成功加鎖。
Mutex 的模式
每個(gè) Mutex 都有兩種模式:Normal, Starving。
Normal 模式
默認(rèn)情況下的模式就是 Normal。 在該模式下,協(xié)程如果加鎖不成功,不會(huì)立即轉(zhuǎn)入阻塞排隊(duì)(先進(jìn)先出),而是判斷是否滿足自旋條件,如果滿足則會(huì)啟動(dòng)自旋過(guò)程,嘗試搶鎖。
Starving 模式
自旋過(guò)程中能搶到鎖,一定意味著同一時(shí)刻有協(xié)程釋放了鎖。我們知道釋放鎖時(shí),如果發(fā)現(xiàn)有阻塞等待的協(xié)程,那么還會(huì)釋放一個(gè)信號(hào)量來(lái)喚醒一個(gè)等待協(xié)程,被喚醒的協(xié)程得到 CPU 后開(kāi)始運(yùn)行,此時(shí)發(fā)現(xiàn)鎖已經(jīng)被搶占了,自己只好再次阻塞,不過(guò)阻塞前會(huì)判斷,自上次阻塞到本次阻塞經(jīng)過(guò)了多長(zhǎng)時(shí)間,如果超過(guò) 1ms,則會(huì)將 Mutex 標(biāo)記為 Starving
模式,然后阻塞。
在Starving
模式下,不會(huì)啟動(dòng)自旋過(guò)程,一旦有協(xié)程釋放了鎖,一定會(huì)喚醒協(xié)程,被喚醒的協(xié)程將成功獲取鎖,同時(shí)會(huì)把等待計(jì)數(shù)減 1。
Woken 狀態(tài)
Woken 狀態(tài)用于加鎖和解鎖過(guò)程中的通信。比如,同一時(shí)刻,兩個(gè)協(xié)程一個(gè)在加鎖,一個(gè)在解鎖,在加鎖的協(xié)程可能在自旋過(guò)程中,此時(shí)把 Woken 標(biāo)記為 1,用于通知解鎖協(xié)程不必釋放信號(hào)量,類(lèi)似知會(huì)一下對(duì)方,不用釋放了,我馬上就拿到鎖了。
到此這篇關(guān)于Golang Mutex 原理詳細(xì)解析的文章就介紹到這了,更多相關(guān)Golang Mutex 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang獲取變量或?qū)ο箢?lèi)型的幾種方式總結(jié)
在golang中并沒(méi)有提供內(nèi)置函數(shù)來(lái)獲取變量的類(lèi)型,但是通過(guò)一定的方式也可以獲取,下面這篇文章主要給大家介紹了關(guān)于golang獲取變量或?qū)ο箢?lèi)型的幾種方式,需要的朋友可以參考下2022-12-12深入探究Golang中flag標(biāo)準(zhǔn)庫(kù)的使用
在本文中,我們將深入探討 flag 標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn)原理和使用技巧,以幫助讀者更好地理解和掌握該庫(kù)的使用方法,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-04-04golang實(shí)現(xiàn)ftp實(shí)時(shí)傳輸文件的案例
這篇文章主要介紹了golang實(shí)現(xiàn)ftp實(shí)時(shí)傳輸文件的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12用golang實(shí)現(xiàn)一個(gè)定時(shí)器任務(wù)隊(duì)列實(shí)例
golang中提供了2種定時(shí)器timer和ticker,分別是一次性定時(shí)器和重復(fù)任務(wù)定時(shí)器。這篇文章主要介紹了用golang實(shí)現(xiàn)一個(gè)定時(shí)器任務(wù)隊(duì)列實(shí)例,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2018-05-05Go 簡(jiǎn)單實(shí)現(xiàn)多租戶數(shù)據(jù)庫(kù)隔離
本文主要介紹了Go 簡(jiǎn)單實(shí)現(xiàn)多租戶數(shù)據(jù)庫(kù)隔離,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05Go框架三件套Gorm?Kitex?Hertz基本用法與常見(jiàn)API講解
這篇文章主要為大家介紹了Go框架三件套Gorm?Kitex?Hertz的基本用法與常見(jiàn)API講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2023-02-02使用go net實(shí)現(xiàn)簡(jiǎn)單的redis通信協(xié)議
本文主要介紹了go net實(shí)現(xiàn)簡(jiǎn)單的redis通信協(xié)議,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12go?doudou開(kāi)發(fā)gRPC服務(wù)快速上手實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了go?doudou開(kāi)發(fā)gRPC服務(wù)快速上手實(shí)現(xiàn)過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12