網(wǎng)站首頁 編程語言 正文
前言
互斥鎖是在并發(fā)程序中對共享資源進(jìn)行訪問控制的主要手段。對此 Go 語言提供了簡單易用的?Mutex
。Mutex 和 Goroutine 合作緊密,概念容易混淆,一定注意要區(qū)分各自的概念。
Mutex
?是一個結(jié)構(gòu)體,對外提供?Lock()
和Unlock()
兩個方法,分別用來加鎖和解鎖。
// 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 是一個互斥鎖,其零值對應(yīng)了未上鎖的狀態(tài),不能被拷貝;
- state 代表互斥鎖的狀態(tài),比如是否被鎖定;
- sema 表示信號量,協(xié)程阻塞會等待該信號量,解鎖的協(xié)程釋放信號量從而喚醒等待信號量的協(xié)程。
注意到 state 是一個 int32 變量,內(nèi)部實(shí)現(xiàn)時把該變量分成四份,用于記錄 Mutex 的狀態(tài)。
- Locked: 表示該 Mutex 是否已經(jīng)被鎖定,0表示沒有鎖定,1表示已經(jīng)被鎖定;
- Woken: 表示是否有協(xié)程已經(jīng)被喚醒,0表示沒有協(xié)程喚醒,1表示已經(jīng)有協(xié)程喚醒,正在加鎖過程中;
- Starving: 表示該 Mutex 是否處于饑餓狀態(tài),0表示沒有饑餓,1表示饑餓狀態(tài),說明有協(xié)程阻塞了超過1ms;
上面三個表示了 Mutex 的三個狀態(tài):鎖定 - 喚醒 - 饑餓。
Waiter 信息雖然也存在 state 中,其實(shí)并不代表狀態(tài)。它表示阻塞等待鎖的協(xié)程個數(shù),協(xié)程解鎖時根據(jù)此值來判斷是否需要釋放信號量。
協(xié)程之間的搶鎖,實(shí)際上爭搶給Locked
賦值的權(quán)利,能給?Locked
?置為1,就說明搶鎖成功。搶不到就阻塞等待?sema
?信號量,一旦持有鎖的協(xié)程解鎖,那么等待的協(xié)程會依次被喚醒。
Woken
?和?Starving
?主要用于控制協(xié)程間的搶鎖過程。
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)被使用,請求 Lock() 的 goroutine 會阻塞,直到鎖可用為止。
單協(xié)程加鎖
若只有一個協(xié)程加鎖,無其他協(xié)程干擾,在加鎖過程中會判斷?Locked
?標(biāo)志位是否為 0,若當(dāng)前為 0 則置為 1,代表加鎖成功。這里本質(zhì)是一個 CAS 操作,依賴了?atomic.CompareAndSwapInt32
。
加鎖被阻塞
假設(shè)協(xié)程B在嘗試加鎖前,已經(jīng)有一個協(xié)程A獲取到了鎖,此時的狀態(tài)為:
此時協(xié)程B嘗試加鎖,被阻塞,Mutex 的狀態(tài)為:
Waiter 計數(shù)器增加了1,協(xié)程B將會持續(xù)阻塞,直到?Locked
?值變成0 后才會被喚醒。
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 沒有被加鎖,就直接?Unlock
?,會拋出一個 runtime error。
從源碼注釋來看,一個 Mutex 并不會與某個特定的 goroutine 綁定,理論上講用一個 goroutine 加鎖,另一個 goroutine 解鎖也是允許的,不過為了代碼可維護(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.
無協(xié)程阻塞下的解鎖
假定在解鎖時,沒有其他協(xié)程阻塞等待加鎖,那么只需要將?Locked
?置為 0 即可,不需要釋放信號量。
解鎖并喚醒協(xié)程
假定解鎖時有1個或多個協(xié)程阻塞,解鎖過程分為兩個步驟:
- 將
Locked
位置0; - 看到?
Waiter
?> 0,釋放一個信號量,喚醒一個阻塞的協(xié)程,被喚醒的協(xié)程把?Locked
?置為1,獲取到鎖。
自旋
加鎖時,如果當(dāng)前?Locked
?位為1,則說明當(dāng)前該鎖由其他協(xié)程持有,嘗試加鎖的協(xié)程并不是馬上轉(zhuǎn)入阻塞,而是會持續(xù)探測?Locked
?位是否變?yōu)?,這個過程就是「自旋」。
自旋的時間很短,如果在自旋過程中發(fā)現(xiàn)鎖已經(jīng)被釋放,那么協(xié)程可以立即獲取鎖。此時即便有協(xié)程被喚醒,也無法獲取鎖,只能再次阻塞。
自旋的好處是,當(dāng)加鎖失敗時不必立即轉(zhuǎn)入阻塞,有一定機(jī)會獲取到鎖,這樣可以避免一部分協(xié)程的切換。
什么是自旋
自旋對應(yīng)于 CPU 的?PAUSE
?指令,CPU 對該指令什么都不做,相當(dāng)于空轉(zhuǎn)。對程序而言相當(dāng)于sleep
了很小一段時間,大概 30個時鐘周期。連續(xù)兩次探測Locked
?位的間隔就是在執(zhí)行這些?PAUSE
?指令,它不同于sleep
,不需要將協(xié)程轉(zhuǎn)為睡眠態(tài)。
自旋條件
加鎖時 Golang 的 runtime 會自動判斷是否可以自旋,無限制的自旋將給 CPU 帶來巨大壓力,自旋必須滿足以下所有條件:
- 自旋次數(shù)要足夠少,通常為 4,即自旋最多 4 次;
- CPU 核數(shù)要大于 1,否則自旋沒有意義,因?yàn)榇藭r不可能有其他協(xié)程釋放鎖;
- 協(xié)程調(diào)度機(jī)制中的 P 的數(shù)量要大于 1,比如使用?
GOMAXPROCS()
?將處理器設(shè)置為 1 就不能啟用自旋; - 協(xié)程調(diào)度機(jī)制中的可運(yùn)行隊(duì)列必須為空,否則會延遲協(xié)程調(diào)度。
可見自旋的條件是很苛刻的,簡單說就是不忙的時候才會啟用自旋。
自旋的優(yōu)勢
自旋的優(yōu)勢是更充分地利用 CPU,盡量避免協(xié)程切換。因?yàn)楫?dāng)前申請加鎖的協(xié)程擁有 CPU,如果經(jīng)過短時間的自旋可以獲得鎖,則當(dāng)前寫成可以繼續(xù)運(yùn)行,不必進(jìn)入阻塞狀態(tài)。
自旋的問題
如果在自旋過程中獲得鎖,那么之前被阻塞的協(xié)程就無法獲得。如果加鎖的協(xié)程特別多,每次都通過自旋獲取鎖,則之前被阻塞的協(xié)程將很難獲取鎖,從而進(jìn)入【饑餓狀態(tài)】。
為此,Golang 1.8 版本后為Mutex
增加了Starving
模式,在這個狀態(tài)下不會自旋,一旦有協(xié)程釋放鎖。那么一定會喚醒一個協(xié)程并成功加鎖。
Mutex 的模式
每個 Mutex 都有兩種模式:Normal,?Starving。
Normal 模式
默認(rèn)情況下的模式就是 Normal。 在該模式下,協(xié)程如果加鎖不成功,不會立即轉(zhuǎn)入阻塞排隊(duì)(先進(jìn)先出),而是判斷是否滿足自旋條件,如果滿足則會啟動自旋過程,嘗試搶鎖。
Starving 模式
自旋過程中能搶到鎖,一定意味著同一時刻有協(xié)程釋放了鎖。我們知道釋放鎖時,如果發(fā)現(xiàn)有阻塞等待的協(xié)程,那么還會釋放一個信號量來喚醒一個等待協(xié)程,被喚醒的協(xié)程得到 CPU 后開始運(yùn)行,此時發(fā)現(xiàn)鎖已經(jīng)被搶占了,自己只好再次阻塞,不過阻塞前會判斷,自上次阻塞到本次阻塞經(jīng)過了多長時間,如果超過 1ms,則會將 Mutex 標(biāo)記為?Starving
模式,然后阻塞。
在Starving
模式下,不會啟動自旋過程,一旦有協(xié)程釋放了鎖,一定會喚醒協(xié)程,被喚醒的協(xié)程將成功獲取鎖,同時會把等待計數(shù)減 1。
Woken 狀態(tài)
Woken 狀態(tài)用于加鎖和解鎖過程中的通信。比如,同一時刻,兩個協(xié)程一個在加鎖,一個在解鎖,在加鎖的協(xié)程可能在自旋過程中,此時把 Woken 標(biāo)記為 1,用于通知解鎖協(xié)程不必釋放信號量,類似知會一下對方,不用釋放了,我馬上就拿到鎖了。
原文鏈接:https://juejin.cn/post/7086756462059323429
相關(guān)推薦
- 2022-07-10 fastmock使用-只能模擬get請求
- 2022-05-08 Python與C語言分別解決完全平方數(shù)問題_python
- 2022-03-27 C++命名空間和缺省參數(shù)介紹_C 語言
- 2022-06-01 C語言線性表中順序表超詳細(xì)理解_C 語言
- 2023-02-25 go-micro微服務(wù)domain層開發(fā)示例詳解_Golang
- 2022-09-27 Go實(shí)現(xiàn)凱撒密碼加密解密_Golang
- 2022-03-20 如何在Go中將[]byte轉(zhuǎn)換為io.Reader_Golang
- 2022-06-16 golang?gorm的預(yù)加載及軟刪硬刪的數(shù)據(jù)操作示例_Golang
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支