go 語(yǔ)言以并發(fā)作為其特性之一晰奖,并發(fā)必然會(huì)帶來(lái)對(duì)于資源的競(jìng)爭(zhēng),這時(shí)候我們就需要使用 go 提供的 sync.Mutex 這把互斥鎖來(lái)保證臨界資源的訪問(wèn)互斥努溃。
既然經(jīng)常會(huì)用這把鎖拆融,那么了解一下其內(nèi)部實(shí)現(xiàn),就能了解這把鎖適用什么場(chǎng)景捻爷,特性如何了。
打開源碼份企,我們先看一下mutex.go文件的描述也榄。
// Mutex fairness.
//
// Mutex can be in 2 modes of operations: normal and starvation.
// In normal mode waiters are queued in FIFO order, but a woken up waiter
// does not own the mutex and competes with new arriving goroutines over
// the ownership. New arriving goroutines have an advantage -- they are
// already running on CPU and there can be lots of them, so a woken up
// waiter has good chances of losing. In such case it is queued at front
// of the wait queue. If a waiter fails to acquire the mutex for more than 1ms,
// it switches mutex to the starvation mode.
//
// In starvation mode ownership of the mutex is directly handed off from
// the unlocking goroutine to the waiter at the front of the queue.
// New arriving goroutines don't try to acquire the mutex even if it appears
// to be unlocked, and don't try to spin. Instead they queue themselves at
// the tail of the wait queue.
//
// If a waiter receives ownership of the mutex and sees that either
// (1) it is the last waiter in the queue, or (2) it waited for less than 1 ms,
// it switches mutex back to normal operation mode.
//
// Normal mode has considerably better performance as a goroutine can acquire
// a mutex several times in a row even if there are blocked waiters.
// Starvation mode is important to prevent pathological cases of tail latency.
//------------------------------------------------------------------------------------------------------------------------------------
// 互斥公平鎖 .
// 鎖有兩種模式,【正常模式】和【饑餓模式】
// 在正常模式下鎖有等待鎖的goroutine都會(huì)進(jìn)入一個(gè)先進(jìn)先出的隊(duì)列(輪流被喚醒)司志,但是被
//喚醒的goroutine不會(huì)直接獲得鎖,而是要跟新到來(lái)的gorotine競(jìng)爭(zhēng)甜紫。
//新來(lái)的goroutine有個(gè)一個(gè)優(yōu)勢(shì) -- 他們已近CPU上運(yùn)行降宅,并且數(shù)量眾多,
//所以剛被喚醒的goroutine大概率獲取不到鎖.在這樣的情況下囚霸,被喚醒的goroutine會(huì)被
//隊(duì)列頭部腰根。如果一個(gè)goroutine等待超過(guò)1ms(寫死的)沒有獲取到鎖,互斥鎖將進(jìn)入饑餓模式拓型。
//
//在饑餓模式中额嘿,解鎖的goroutine會(huì)將鎖直接交付給等待隊(duì)里最前面的goroutine.
//新來(lái)的goroutine 不會(huì)嘗試獲取鎖(即使鎖在空閑狀態(tài)),也不會(huì)進(jìn)行自旋吨述,
//他們只是加入到等待隊(duì)列尾部.
//
//如果一個(gè)goroutine 獲取到鎖岩睁,他會(huì)判斷
//1 . 他是否是位于等待隊(duì)列末尾
//2 . 他等待是否超過(guò)1ms
// 以上只有有一個(gè)成立,將把互斥鎖切換至正常模式
//
// 正常模式 :具有較好的性能揣云,即使存在許多阻塞者,goroutine也也會(huì)嘗試幾次獲取鎖冰啃。
// 饑餓模式 :對(duì)于防止尾部延遲是非常重要的邓夕。
sync.Mutex
// A Mutex 是一個(gè)互斥鎖
// 0 值代碼表未加鎖轉(zhuǎn)態(tài)
//
//互斥鎖在第一次被使用后不能被復(fù)制.
type Mutex struct {
state int32
sema uint32
}
const (
mutexLocked = 1 << iota // mutex is locked state第1位
mutexWoken //state第2位
mutexStarving //state第3位
mutexWaiterShift = iota
starvationThresholdNs = 1e6
)
stage
這個(gè)字段會(huì)同時(shí)被多個(gè)goroutine公用(使用atomic來(lái)保證原子性),第1個(gè)bit 表示已加鎖阎毅。第2個(gè)bit 表示某個(gè)goroutine被喚醒焚刚,嘗試獲取鎖,第3個(gè)bit表示這把鎖是否是饑餓狀態(tài)扇调。
[1][1][1]
: 第一個(gè)[1] 表示鎖狀態(tài)矿咕,第二個(gè)[1]表示是否有喚醒,第三個(gè)[1]表示是否是饑餓模式
·001
普通模式 狼钮,無(wú)喚醒碳柱, 鎖 ,010
普通模式熬芜, 有喚醒 莲镣,無(wú)鎖狀態(tài),,101
饑餓模式 涎拉,無(wú)喚醒 瑞侮,鎖
sema
用來(lái)喚醒 goroutine 所用的信號(hào)量。
LOCK
在看代碼之前鼓拧,我們需要有一個(gè)概念:每個(gè) goroutine 也有自己的狀態(tài)半火,存在局部變量里面(也就是函數(shù)棧里面),goroutine 有可能是新到的季俩、被喚醒的钮糖、正常的、饑餓的种玛。
func (m *Mutex) Lock() {
// 如果鎖是空閑狀態(tài)藐鹤,直接獲取鎖 通過(guò) atomic.CompareAndSwapInt32 保證原子性
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
//用來(lái)保存 goroutine 等待時(shí)間
var waitStartTime int64
// 用來(lái)存當(dāng)前goroutine是否饑餓
starving := false
// 用來(lái)存當(dāng)前goroutine是否已喚醒
awoke := false
// 用來(lái)存當(dāng)前goroutine的循環(huán)次數(shù)
iter := 0
// 復(fù)制一下當(dāng)前鎖的狀態(tài)
old := m.state
//自旋起來(lái)
for {
// Don't spin in starvation mode, ownership is handed off to waiter
//[翻譯] 在饑餓模式下就不要自旋了瓤檐,因?yàn)殒i會(huì)直接被交付
// so we won't be able to acquire the mutex anyway.
//[翻譯] 所以自旋也獲取不到鎖
// 第一個(gè)條件是state已被鎖,但是不是饑餓狀態(tài)娱节。如果時(shí)饑餓狀態(tài)挠蛉,自旋時(shí)沒有用的,鎖的擁有權(quán)直接交給了等待隊(duì)列的第一個(gè)肄满。
// 第二個(gè)條件是還可以自旋谴古,多核、壓力不大并且在一定次數(shù)內(nèi)可以自旋稠歉,
// 如果滿足這兩個(gè)條件掰担,不斷自旋來(lái)等待鎖被釋放、或者進(jìn)入饑餓狀態(tài)怒炸、或者不能再自旋带饱。
// [偽代碼]:if isLocked() and isNotStarving() and canSpin()
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
// Active spinning makes sense.
//[翻譯] 主動(dòng)自旋是有意義的
// Try to set mutexWoken flag to inform Unlock
//[翻譯] 嘗試修改喚醒標(biāo)志
// to not wake other blocked goroutines.
//[翻譯] 這樣就可以不喚醒其他goroutines
// 自旋的過(guò)程中如果發(fā)現(xiàn)state還沒有設(shè)置woken標(biāo)識(shí),則設(shè)置它的woken標(biāo)識(shí)阅羹, 并標(biāo)記自己為被喚醒勺疼。
// atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) 這段代碼來(lái)修改 mutex stage 第2位 設(shè)置有喚醒標(biāo)識(shí)。這樣就不會(huì)去喚醒其他的goroutine了
if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&
atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {
awoke = true
}
//進(jìn)行自旋
runtime_doSpin()
//循環(huán)迭代計(jì)數(shù)
iter++
//更新鎖的狀態(tài) 捏鱼,因?yàn)樵谶@段時(shí)間里鎖的狀態(tài)可能被其他goroutine修改了
old = m.state
continue
}
//到了這一步执庐,state的狀態(tài)可能是
//1. 未加鎖 ,普通模式
//2. 未加鎖导梆,饑餓模式
//3. 已加鎖轨淌,饑餓模式
//4. 已加鎖,普通模式 (可能做上面執(zhí)行時(shí)看尼,鎖被其他goroutine 獲取了)
//獲取鎖的最新狀態(tài)递鹉,這個(gè)字段用來(lái)存儲(chǔ)希望設(shè)置鎖的狀態(tài)
new := old
// Don't try to acquire starving mutex, new arriving goroutines must queue.
//[翻譯]不要饑餓模式下獲取鎖,新來(lái)的去排隊(duì)
// [偽代碼]:if isNotStarving() 狡忙,也就是說(shuō)是饑餓狀態(tài)時(shí)不獲取鎖
if old&mutexStarving == 0 {
//new 設(shè)置為獲取鎖狀態(tài)
new |= mutexLocked
}
//如果是鎖狀態(tài)梳虽,或者是饑餓狀態(tài),就設(shè)置等待隊(duì)列+1 灾茁,(此時(shí)就是等位 +1)
if old&(mutexLocked|mutexStarving) != 0 {
new += 1 << mutexWaiterShift
}
// The current goroutine switches mutex to starvation mode.
//[翻譯] 當(dāng)前goroutine 將所切換至饑餓模式
// But if the mutex is currently unlocked, don't do the switch.
//[翻譯]但是如果鎖的狀態(tài)是unlocked 就不要切換窜觉。
// Unlock expects that starving mutex has waiters, which will not be true in this case.
//[翻譯] unlock 期望一個(gè)饑餓模式的gorutine時(shí),這個(gè)例子就不成立了(也就說(shuō)如果有其他的goroutine將鎖切換成饑餓模式)
//[偽代碼] isStarving and isLock
if starving && old&mutexLocked != 0 {
new |= mutexStarving
}
//如果當(dāng)前goroutine 是喚醒狀態(tài)北专,那么我要resest這個(gè)狀態(tài)
//因?yàn)間oroutine要么是拿到鎖了禀挫,要么是進(jìn)入sleep了
if awoke {
// The goroutine has been woken from sleep,
//[翻譯]goroutine 已近被喚醒了。
// so we need to reset the flag in either case.
//[翻譯]所以我們要切換狀態(tài)了
if new&mutexWoken == 0 {
throw("sync: inconsistent mutex state")
}
//設(shè)置成非喚醒狀態(tài)
new &^= mutexWoken
}
// 通過(guò)CAS來(lái)嘗試設(shè)置鎖的狀態(tài)
// 這里可能是設(shè)置鎖拓颓,也有可能是只設(shè)置為饑餓狀態(tài)和等待數(shù)量
if atomic.CompareAndSwapInt32(&m.state, old, new) {
// 如果old state的狀態(tài)是未被鎖狀態(tài)语婴,并且鎖不處于饑餓狀態(tài),
// 那么當(dāng)前goroutine已經(jīng)獲取了鎖的擁有權(quán),返回
if old&(mutexLocked|mutexStarving) == 0 {
break // locked the mutex with CAS
}
// If we were already waiting before, queue at the front of the queue.
//[翻譯] 如果我們已經(jīng)在排隊(duì)了,就排在隊(duì)伍的最前面砰左。
queueLifo := waitStartTime != 0
if waitStartTime == 0 {
//計(jì)算等待時(shí)間
waitStartTime = runtime_nanotime()
}
// 既然未能獲取到鎖匿醒, 那么就使用sleep原語(yǔ)阻塞本goroutine
// 如果是新來(lái)的goroutine,queueLifo=false, 加入到等待隊(duì)列的尾部,耐心等待
// 如果是喚醒的goroutine, queueLifo=true, 加入到等待隊(duì)列的頭部
runtime_SemacquireMutex(&m.sema, queueLifo)
//如果當(dāng)前是饑餓狀態(tài)缠导,并且等待超過(guò)1ms
starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs
// 得到當(dāng)前的鎖狀態(tài)
old = m.state
if old&mutexStarving != 0 {
// If this goroutine was woken and mutex is in starvation mode,
//[翻譯] 如果這個(gè)goroutine喚醒廉羔,并且鎖是饑餓模式
// ownership was handed off to us but mutex is in somewhat
//[翻譯] 鎖會(huì)直接傳遞給我們
// inconsistent state: mutexLocked is not set and we are still
//[翻譯] 如果鎖處于不一致狀態(tài),那么會(huì)出現(xiàn)問(wèn)題
// accounted as waiter. Fix that.
if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {
throw("sync: inconsistent mutex state")
}
// 當(dāng)前的goroutine獲得了鎖僻造,那么就把等待隊(duì)列-1
delta := int32(mutexLocked - 1<<mutexWaiterShift)
//如果是最后一個(gè)等待者憋他,就退出饑餓模式
if !starving || old>>mutexWaiterShift == 1 {
// Exit starvation mode.
//[翻譯] 退出饑餓模式
// Critical to do it here and consider wait time.
//[翻譯]重要的是要在這里做,并考慮等待時(shí)間髓削。
// Starvation mode is so inefficient, that two goroutines can go lock-step infinitely
//[翻譯]饑餓模式非常低效竹挡,兩個(gè)goroutine一旦切換到饑餓模式,就會(huì)無(wú)限地執(zhí)行鎖步立膛。
delta -= mutexStarving
}
//加鎖
atomic.AddInt32(&m.state, delta)
break
}
// 如果鎖不是饑餓模式揪罕,就把當(dāng)前的goroutine設(shè)為被喚醒
// 并且重置iter(重置spin)
awoke = true
iter = 0
} else {
// 如果CAS不成功,也就是說(shuō)沒能成功獲得鎖旧巾,鎖被別的goroutine獲得了或者鎖一直沒被釋放
// 那么就更新狀態(tài)耸序,重新開始循環(huán)嘗試拿鎖
old = m.state
}
}
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
}
UNLOCK
接下來(lái)我們來(lái)看看 Unlock 的實(shí)現(xiàn),對(duì)于 Unlock 來(lái)說(shuō)鲁猩,有兩個(gè)比較關(guān)鍵的特性:
如果說(shuō)鎖不是處于 locked 狀態(tài),那么對(duì)鎖執(zhí)行 Unlock 會(huì)導(dǎo)致 panic罢坝;
鎖和 goroutine 沒有對(duì)應(yīng)關(guān)系廓握,所以我們完全可以在 goroutine 1 中獲取到鎖,然后在 goroutine 2 中調(diào)用 Unlock 來(lái)釋放鎖(這是什么騷操作`夷稹)
// Unlock unlocks m.
// [翻譯] 解鎖
// It is a run-time error if m is not locked on entry to Unlock.
//[翻譯] 如果沒有l(wèi)ocked 執(zhí)行 unlock 會(huì)有一個(gè)run-time error
// A locked Mutex is not associated with a particular goroutine.
//[翻譯] 一個(gè)被鎖的互斥對(duì)象與一個(gè)特定的goroutine沒有關(guān)聯(lián)隙券。
// It is allowed for one goroutine to lock a Mutex and then
//[翻譯] 允許其他的goroutine進(jìn)行解鎖
// arrange for another goroutine to unlock it.
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+mutexLocked)&mutexLocked == 0 {
throw("sync: unlock of unlocked mutex")
}
//解鎖成功,并且不是解鎖狀態(tài)
if new&mutexStarving == 0 {
//復(fù)制鎖狀態(tài)
old := new
for {
// If there are no waiters or a goroutine has already
// been woken or grabbed the lock, no need to wake anyone.
// In starvation mode ownership is directly handed off from unlocking
// goroutine to the next waiter. We are not part of this chain,
// since we did not observe mutexStarving when we unlocked the mutex above.
// So get off the way.
// [翻譯] 如果沒有沒有等待著闹司,或者沒有喚醒的goroutine娱仔,不用喚醒任何人。
//如果沒有其他的goroutine 加鎖游桩。
//在饑餓模式下鎖會(huì)被直接傳遞牲迫,但是我們這里不關(guān)注饑餓模式下的設(shè)置,
if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {
return
}
// Grab the right to wake someone.
// 走到這一步的時(shí)候借卧,說(shuō)明鎖目前還是空閑狀態(tài)盹憎,并且沒有g(shù)oroutine被喚醒且隊(duì)列中有g(shù)oroutine等待拿鎖
// 那么我們就要把鎖的狀態(tài)設(shè)置為被喚醒,等待隊(duì)列-1
new = (old - 1<<mutexWaiterShift) | mutexWoken
// 如果狀態(tài)設(shè)置成功了铐刘,我們就通過(guò)信號(hào)量去喚醒goroutine
if atomic.CompareAndSwapInt32(&m.state, old, new) {
runtime_Semrelease(&m.sema, false)
return
}
// 循環(huán)結(jié)束的時(shí)候陪每,更新一下狀態(tài),因?yàn)橛锌赡茉趫?zhí)行的過(guò)程中,狀態(tài)被修改了(比如被Lock改為了饑餓狀態(tài))
old = m.state
}
} else {
// 饑餓模式下檩禾, 直接將鎖的擁有權(quán)傳給等待隊(duì)列中的第一個(gè).
// 注意此時(shí)state的mutexLocked還沒有加鎖挂签,喚醒的goroutine會(huì)設(shè)置它。
// 在此期間盼产,如果有新的goroutine來(lái)請(qǐng)求鎖饵婆, 因?yàn)閙utex處于饑餓狀態(tài), mutex還是被認(rèn)為處于鎖狀態(tài)辆飘,
// 新來(lái)的goroutine不會(huì)把鎖搶過(guò)去.
runtime_Semrelease(&m.sema, true)
}
}
結(jié)語(yǔ)
鎖和解鎖的代碼只有這么簡(jiǎn)單的幾行啦辐,但是其中的原來(lái)和設(shè)計(jì)的巧妙點(diǎn)缺非常多,從這個(gè)里我們可以看出蜈项,系統(tǒng)設(shè)計(jì)的好壞跟代碼多少無(wú)關(guān)芹关,系統(tǒng)內(nèi)涵的設(shè)計(jì)跟代碼設(shè)計(jì)也無(wú)關(guān),真的大師一定是大道至簡(jiǎn)紧卒。