mutex工作機制
Mutex有兩種工作模式:正常模式和飢餓模式
在正常模式中,等待着按照FIFO的順序排隊獲取鎖,但是一個被喚醒的等待者有時候並不能獲取mutex,它還需要和新到來的goroutine們競爭mutex的使用權。新到來的goroutine有一個優勢,因爲新到達的goroutine已經在CPU上運行了,因此一個被喚醒的等待者有很大的概率獲取不到鎖。在這種情況下它處在等待隊列的前面。如果一個goroutine等待mutex釋放的時間超過1ms,它就會將mutex切換到飢餓模式。
在飢餓模式中,mutex的所有權直接從解鎖的goroutine遞交到等待隊列中排在最前方的goroutine。新到達的goroutine們不要嘗試去獲取mutex,即便它看起來是解鎖狀態,也不要嘗試自旋,而是排到等待隊列的尾部。
在飢餓模式下,有一個goroutine獲取到mutex鎖了,如果它滿足下條件中的任意一個,mutex將會切換回去正常模式:
- 等待隊列只有一個goroutine
- goroutine的等待時間小於1ms
正常模式有更好的性能,因爲goroutine可以連續多次獲得mutex鎖,以及避免多個線程的排隊消耗。
飢餓模式需要預防隊列尾部goroutine一致無法獲取mutex鎖的問題。
mutex數據結構以及調用的函數
type Mutex struct {
state int32 //將一個32位整數拆分爲:從最高位排列
//當前阻塞的goroutine數(29位)
//飢餓狀態(1位)
//喚醒狀態(1位)
//鎖狀態(1位)
sema uint32 // 信號量
}
const (
mutexLocked = 1 << iota // 用最後一位表示當前鎖的狀態,0-未鎖住 1-已鎖住
mutexWoken // 用倒數第二位表示當前鎖是否被喚醒 0-喚醒 1-未喚醒
mutexStarving // 用倒數第三位表示當前鎖是否爲飢餓模式,0爲正常模式,1爲飢餓模式。
mutexWaiterShift = iota // 3,從倒數第四位往前的bit位表示在排隊等待的goroutine數
starvationThresholdNs = 1e6 // 1ms
)
- runtime_canSpin。判斷是否需要自選,golang中自旋鎖並不會一直自旋下去,在runtime包中runtime_canSpin方法做了一些限制, 傳遞過來的iter大等於4或者cpu核數小等於1,最大邏輯處理器大於1(多核),至少有個本地的P隊列,並且本地的P隊列可運行G隊列爲空纔會進行自旋。
func sync_runtime_canSpin(i int) bool {
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
}
- runtime_doSpin。 會調用
procyield
函數,該函數也是彙編語言實現。函數內部[循環]調用PAUSE指令。PAUSE指令什麼都不做,但是會消耗CPU時間,在執行PAUSE指令時,CPU不會對它做不必要的優化。
func sync_runtime_doSpin() {
procyield(active_spin_cnt)
}
- runtime_SemacquireMutex。 一個gotoutine的等待隊列,如果lifo爲true,則插入隊列頭,否則插入隊尾
func runtime_SemacquireMutex(s *uint32, lifo bool)
- runtime_Semrelease。喚醒被runtime_SemacquireMutex函數掛起的等待goroutine,如果handoff爲true,喚醒隊列頭第一個等待者,否則的話可能是隨機
func runtime_Semrelease(s *uint32, handoff bool)
Lock方法實現
Lock方法申請對mutex加鎖,Lock執行的時候,分三種情況:
- 無衝突。通過CAS操作把當前狀態設置爲加鎖狀態。
- 有衝突。開始runtime_canSpin自旋,並等待鎖釋放,如果其他goroutine在這段時間內釋放了該鎖,直接獲得該鎖;如果沒有釋放進入第3步。
- 有衝突,且已經過了自旋階段。通過調用seamacquire函數來讓當前goroutine進入等待狀態。
// 無衝突
// CompareAndSwapInt32相等才賦值,且返回true
// 查看 state 是否爲0(空閒狀態), 如果是則表示可以加鎖,將其鎖置爲鎖定狀態
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled { // 數據競爭檢測
race.Acquire(unsafe.Pointer(m))
}
return
}
var waitStartTime int64 // 當前goroutine開始等待時間
starving := false // goroutine當前所處的模式
awoke := false // 當前goroutine是否被喚醒
iter := 0 // 自旋迭代的次數
old := m.state // old 保存當前 mutex 的狀態
for {
// 有衝突,開始自旋狀態
// 當mutex處於鎖定非飢餓工作模式且支持自旋操作的時候。其實就是在自旋然後等待別人釋放鎖,如果有人釋放鎖,則會立刻進行下面的嘗試獲取鎖的邏輯
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// 將 mutex.state 的倒數第二位設置爲1,用來告知 Unlock 操作,存在 goroutine 即將得到鎖,不需要喚醒其他goroutine
// 當前線程沒有處於喚醒狀態,當前鎖處於喚醒狀態,當前有正在等待的goroutine
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
// 進入自旋
runtime_doSpin()
iter++
old = m.state
continue
}
// 有衝突,且已經過了自旋階段。走到這裏出現三種情況:1. 當前鎖是未鎖定狀態;2. 鎖是飢餓模式;3. 自旋超過指定的次數,不再允許自旋。
new := old
// 1. 如果當前不是飢餓模式,則這裏其實就可以嘗試進行鎖的獲取了|=其實就是將鎖的那個bit位設爲1表示鎖定狀態
if old&mutexStarving == 0 {
new |= mutexLocked
}
// 當mutex處於加鎖或飢餓狀態的時候,新到來的goroutine進入等待隊列
// 2.此處需要判斷是否爲加鎖狀態,因爲從1到2的時候可能mutex 重新被其他goroutine加鎖了
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift //等待隊列加1操作
}
// 如果當前goroutine已經處於飢餓狀態,並且當前鎖還是被佔用,則鎖嘗試進行飢餓模式的切換,但如果當前 mutex 未鎖定,則不需要切換。Unlock操作希望飢餓模式存在等待者
// 3.starving條件是爲了防止: 如果在2處判斷mutex沒有處於加鎖,而在這裏判斷mutex卻加鎖了,這時候加入飢餓模式,可是goroutine沒有入列
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
// awoke爲true則表明當前線程在上面自旋的時候,修改mutexWoken狀態成功,清除喚醒標誌位
// 爲什麼要清除標誌位呢?實際上是因爲後續流程很有可能當前線程會被掛起,就需要等待其他釋放鎖的goroutine來喚醒
// 但如果unlock的時候發現mutexWoken的位置不是0,則就不會去喚醒,則該線程就無法再醒來加鎖
if awoke {
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
// &^操作:後面對應的位爲1,則清0前面對應的位,若後面對應的位爲0,則前面對應的位保持不變
new &^= mutexWoken
}
// 調用CAS更新state狀態
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果原來的狀態等於0則表明當前已經釋放了鎖並且也不處於飢餓模式下,表明可以加鎖成功,退出CAS
if old&(mutexLocked|mutexStarving) == 0 {
break
}
// 排隊邏輯,如果發現waitStatrTime不爲0,則表明當前線程之前已經在排隊來,後面可能因爲unlock被喚醒,但是本次依舊沒獲取到鎖,所以就將它移動到等待隊列的頭部
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
// 記錄開始等待時間
waitStartTime = runtime_nanotime()
}
// 將被喚醒卻沒得到鎖的 goroutine 插入當前等待隊列的最前端
runtime_SemacquireMutex(&m.sema, queueLifo, 1)
// 如果當前goroutine等待時間超過starvationThresholdNs,mutex 進入飢餓模式
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 重新獲取狀態
old = m.state
// 如果發現當前已經是飢餓模式,注意飢餓模式喚醒的是第一個goroutine
if old&mutexStarving != 0 {
// 一致性檢查
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
delta := int32(mutexLocked - 1<<mutexWaiterShift)
// 如果不是飢餓模式了或者當前等待着只剩下一個,退出飢餓模式
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
// 更新狀態
atomic.AddInt32(&m.state, delta)
break
}
awoke = true
iter = 0
} else {
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
Unlock方法實現
func (m *Mutex) Unlock() {
if race.Enabled {
_ = m.state
race.Release(unsafe.Pointer(m))
}
// 直接進行cas操作: mutex 的state減去1, 加鎖狀態 -> 未加鎖
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
if (new+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
// 正常模式釋放
if new&mutexStarving == 0 {
old := new
for {
// 如果沒有等待者,或者已經存在一個 goroutine 被喚醒或得到鎖,或處於飢餓模式,無需喚醒任何處於等待狀態的 goroutine
// 因爲lock方法存在自旋一直在獲取鎖,所以可能解鎖後就已經有goroutine獲取到鎖了
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// 減去一個等待計數,然後將當前模式切換成mutexWoken
new = (old - 1<<mutexWaiterShift) | mutexWoken
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 隨機喚醒一個阻塞的 goroutine
runtime_Semrelease(&m.sema, false)
return
}
old = m.state
}
// 飢餓模式喚醒
} else {
// 喚醒第一個等待的線程
runtime_Semrelease(&m.sema, true)
}
}
}
參考文獻
golang mutex源碼詳細解析
golang之sync.Mutex互斥鎖源碼分析
go sync.Mutex 設計思想與演化過程 (一)
sync包 mutex源碼閱讀
圖解Go裏面的互斥鎖mutex瞭解編程語言核心實現源碼