go mutex互斥鎖使用Lock和Unlock方法占有釋放資源
mutex 初步認(rèn)識
mutex 的源碼主要是在 src/sync/mutex.go
文件里,它的結(jié)構(gòu)體比較簡單,如下:
type Mutex struct { state int32 sema uint32 }
我們可以看到有一個(gè)字段 sema,它表示信號量標(biāo)記位。所謂的信號量是用于 Goroutine 之間阻塞或喚醒的。這有點(diǎn)像操作系統(tǒng)里的 PV 原語操作,我們先來認(rèn)識下 PV 原語操作:
PV 原語解釋:
通過操作信號量 S 來處理進(jìn)程間的同步與互斥的問題。
S>0:表示有 S 個(gè)資源可用;S=0 表示無資源可用;S<0 絕對值表示等待隊(duì)列或鏈表中的進(jìn)程個(gè)數(shù)。信號量 S 的初值應(yīng)大于等于 0。
P 原語:表示申請一個(gè)資源,對 S 原子性的減 1,若 減 1 后仍 S>=0,則該進(jìn)程繼續(xù)執(zhí)行;若 減 1 后 S<0,表示已無資源可用,需要將自己阻塞起來,放到等待隊(duì)列上。
V 原語:表示釋放一個(gè)資源,對 S 原子性的加 1;若 加 1 后 S>0,則該進(jìn)程繼續(xù)執(zhí)行;若 加 1 后 S<=0,表示等待隊(duì)列上有等待進(jìn)程,需要將第一個(gè)等待的進(jìn)程喚醒。
通過上面的解釋,mutex 就可以利用信號量來實(shí)現(xiàn) goroutine 的阻塞和喚起了。
其實(shí) mutex 本質(zhì)上就是一個(gè)關(guān)于信號量的阻塞喚起操作。
當(dāng) goroutine 不能占有鎖資源的時(shí)候會被阻塞掛起,此時(shí)不能繼續(xù)執(zhí)行后面的代碼邏輯。
當(dāng) mutex 釋放鎖資源時(shí),則會繼續(xù)喚起之前的 goroutine 去搶占鎖資源。
至于 mutex 的 state 狀態(tài)字段則是用來做狀態(tài)流轉(zhuǎn)的,這些狀態(tài)值涉及到了一些概念,下面我們具體來解釋一番。
mutex 狀態(tài)標(biāo)志位
mutex 的 state 有 32 位,它的低 3 位分別表示 3 種狀態(tài):喚醒狀態(tài)、上鎖狀態(tài)、饑餓狀態(tài),剩下的位數(shù)則表示當(dāng)前阻塞等待的 goroutine 數(shù)量。
mutex 會根據(jù)當(dāng)前的 state 狀態(tài)來進(jìn)入正常模式、饑餓模式或者是自旋。
mutex 正常模式
當(dāng) mutex 調(diào)用 Unlock() 方法釋放鎖資源時(shí),如果發(fā)現(xiàn)有等待喚起的 Goroutine 隊(duì)列時(shí),則會將隊(duì)頭的 Goroutine 喚起。
隊(duì)頭的 goroutine 被喚起后,會調(diào)用 CAS 方法去嘗試性的修改 state 狀態(tài),如果修改成功,則表示占有鎖資源成功。
(注:CAS 在 Go 里用 atomic.CompareAndSwapInt32(addr *int32, old, new int32) 方法實(shí)現(xiàn),CAS 類似于樂觀鎖作用,修改前會先判斷地址值是否還是 old 值,只有還是 old 值,才會繼續(xù)修改成 new 值,否則會返回 false 表示修改失敗。)
mutex 饑餓模式
由于上面的 Goroutine 喚起后并不是直接的占用資源,還需要調(diào)用 CAS 方法去嘗試性占有鎖資源。如果此時(shí)有新來的 Goroutine,那么它也會調(diào)用 CAS 方法去嘗試性的占有資源。
但對于 Go 的調(diào)度機(jī)制來講,會比較偏向于 CPU 占有時(shí)間較短的 Goroutine 先運(yùn)行,而這將造成一定的幾率讓新來的 Goroutine 一直獲取到鎖資源,此時(shí)隊(duì)頭的 Goroutine 將一直占用不到,導(dǎo)致餓死。
針對這種情況,Go 采用了饑餓模式。即通過判斷隊(duì)頭 Goroutine 在超過一定時(shí)間后還是得不到資源時(shí),會在 Unlock 釋放鎖資源時(shí),直接將鎖資源交給隊(duì)頭 Goroutine,并且將當(dāng)前狀態(tài)改為饑餓模式。
后面如果有新來的 Goroutine 發(fā)現(xiàn)是饑餓模式時(shí), 則會直接添加到等待隊(duì)列的隊(duì)尾。
mutex 自旋
如果 Goroutine 占用鎖資源的時(shí)間比較短,那么每次都調(diào)用信號量來阻塞喚起 goroutine,將會很浪費(fèi)資源。
因此在符合一定條件后,mutex 會讓當(dāng)前的 Goroutine 去空轉(zhuǎn) CPU,在空轉(zhuǎn)完后再次調(diào)用 CAS 方法去嘗試性的占有鎖資源,直到不滿足自旋條件,則最終會加入到等待隊(duì)列里。
自旋的條件如下:
- 還沒自旋超過 4 次
- 多核處理器
- GOMAXPROCS > 1
- p 上本地 Goroutine 隊(duì)列為空
可以看出,自旋條件還是比較嚴(yán)格的,畢竟這會消耗 CPU 的運(yùn)算能力。
mutex 的 Lock() 過程
首先,如果 mutex 的 state = 0,即沒有誰在占有資源,也沒有阻塞等待喚起的 goroutine。則會調(diào)用 CAS 方法去嘗試性占有鎖,不做其他動(dòng)作。
如果不符合 m.state = 0,則進(jìn)一步判斷是否需要自旋。
當(dāng)不需要自旋又或者自旋后還是得不到資源時(shí),此時(shí)會調(diào)用 runtime_SemacquireMutex 信號量函數(shù),將當(dāng)前的 goroutine 阻塞并加入等待喚起隊(duì)列里。
當(dāng)有鎖資源釋放,mutex 在喚起了隊(duì)頭的 goroutine 后,隊(duì)頭 goroutine 會嘗試性的占有鎖資源,而此時(shí)也有可能會和新到來的 goroutine 一起競爭。
當(dāng)隊(duì)頭 goroutine 一直得不到資源時(shí),則會進(jìn)入饑餓模式,直接將鎖資源交給隊(duì)頭 goroutine,讓新來的 goroutine 阻塞并加入到等待隊(duì)列的隊(duì)尾里。
對于饑餓模式將會持續(xù)到?jīng)]有阻塞等待喚起的 goroutine 隊(duì)列時(shí),才會解除。
Unlock 過程
mutex 的 Unlock() 則相對簡單。同樣的,會先進(jìn)行快速的解鎖,即沒有等待喚起的 goroutine,則不需要繼續(xù)做其他動(dòng)作。
如果當(dāng)前是正常模式,則簡單的喚起隊(duì)頭 Goroutine。如果是饑餓模式,則會直接將鎖交給隊(duì)頭 Goroutine,然后喚起隊(duì)頭 Goroutine,讓它繼續(xù)運(yùn)行。
mutex 代碼詳解
好了,上面大體流程講完了,下面將會把詳細(xì)的代碼流程呈上,讓大家能更詳細(xì)的知道 mutex 的 Lock()、Unlock() 方法邏輯。
mutex Lock() 代碼詳解
// Lock mutex 的鎖方法。 func (m *Mutex) Lock() { // 快速上鎖. if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // 快速上鎖失敗,將進(jìn)行操作較多的上鎖動(dòng)作。 m.lockSlow() } func (m *Mutex) lockSlow() { var waitStartTime int64 // 記錄當(dāng)前 goroutine 的等待時(shí)間 starving := false // 是否饑餓 awoke := false // 是否被喚醒 iter := 0 // 自旋次數(shù) old := m.state // 當(dāng)前 mutex 的狀態(tài) for { // 當(dāng)前 mutex 的狀態(tài)已上鎖,并且非饑餓模式,并且符合自旋條件 if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { // 當(dāng)前還沒設(shè)置過喚醒標(biāo)識 if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { awoke = true } runtime_doSpin() iter++ old = m.state continue } new := old // 如果不是饑餓狀態(tài),則嘗試上鎖 // 如果是饑餓狀態(tài),則不會上鎖,因?yàn)楫?dāng)前的 goroutine 將會被阻塞并添加到等待喚起隊(duì)列的隊(duì)尾 if old&mutexStarving == 0 { new |= mutexLocked } // 等待隊(duì)列數(shù)量 + 1 if old&(mutexLocked|mutexStarving) != 0 { new += 1 << mutexWaiterShift } // 如果 goroutine 之前是饑餓模式,則此次也設(shè)置為饑餓模式 if starving && old&mutexLocked != 0 { new |= mutexStarving } // if awoke { // 如果狀態(tài)不符合預(yù)期,則報(bào)錯(cuò) if new&mutexWoken == 0 { throw("sync: inconsistent mutex state") } // 新狀態(tài)值需要清除喚醒標(biāo)識,因?yàn)楫?dāng)前 goroutine 將會上鎖或者再次 sleep new &^= mutexWoken } // CAS 嘗試性修改狀態(tài),修改成功則表示獲取到鎖資源 if atomic.CompareAndSwapInt32(&m.state, old, new) { // 非饑餓模式,并且未獲取過鎖,則說明此次的獲取鎖是 ok 的,直接 return if old&(mutexLocked|mutexStarving) == 0 { break } // 根據(jù)等待時(shí)間計(jì)算 queueLifo queueLifo := waitStartTime != 0 if waitStartTime == 0 { waitStartTime = runtime_nanotime() } // 到這里,表示未能上鎖成功 // queueLife = true, 將會把 goroutine 放到等待隊(duì)列隊(duì)頭 // queueLife = false, 將會把 goroutine 放到等待隊(duì)列隊(duì)尾 runtime_SemacquireMutex(&m.sema, queueLifo, 1) // 計(jì)算是否符合饑餓模式,即等待時(shí)間是否超過一定的時(shí)間 starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNs old = m.state // 上一次是饑餓模式 if old&mutexStarving != 0 { if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 { throw("sync: inconsistent mutex state") } delta := int32(mutexLocked - 1<<mutexWaiterShift) // 此次不是饑餓模式又或者下次沒有要喚起等待隊(duì)列的 goroutine 了 if !starving || old>>mutexWaiterShift == 1 { delta -= mutexStarving } atomic.AddInt32(&m.state, delta) break } // 此處已不再是饑餓模式了,清除自旋次數(shù),重新到 for 循環(huán)競爭鎖。 awoke = true iter = 0 } else { old = m.state } } if race.Enabled { race.Acquire(unsafe.Pointer(m)) } }
mutex Unlock() 代碼詳解
// Unlock 對 mutex 解鎖. // 如果沒有上過鎖,缺調(diào)用此方法解鎖,將會拋出運(yùn)行時(shí)錯(cuò)誤。 // 它將允許在不同的 Goroutine 上進(jìn)行上鎖解鎖 func (m *Mutex) Unlock() { if race.Enabled { _ = m.state race.Release(unsafe.Pointer(m)) } // 快速嘗試解鎖 new := atomic.AddInt32(&m.state, -mutexLocked) if new != 0 { // 快速解鎖失敗,將進(jìn)行操作較多的解鎖動(dòng)作。 m.unlockSlow(new) } } func (m *Mutex) unlockSlow(new int32) { // 非上鎖狀態(tài),直接拋出異常 if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } // 正常模式 if new&mutexStarving == 0 { old := new for { // 沒有需要喚起的等待隊(duì)列 if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { return } // 喚起等待隊(duì)列并數(shù)量-1 new = (old - 1<<mutexWaiterShift) | mutexWoken if atomic.CompareAndSwapInt32(&m.state, old, new) { runtime_Semrelease(&m.sema, false, 1) return } old = m.state } } else { //饑餓模式,將鎖直接給等待隊(duì)列的隊(duì)頭 goroutine runtime_Semrelease(&m.sema, true, 1) } }
以上就是go mutex互斥鎖使用Lock和Unlock方法占有釋放資源的詳細(xì)內(nèi)容,更多關(guān)于golang mutex互斥鎖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
GO使用socket和channel實(shí)現(xiàn)簡單控制臺聊天室
今天小編給大家分享一個(gè)簡單的聊天室功能,聊天室主要功能是用戶可以加入離開聊天室,實(shí)現(xiàn)思路也很簡單明了,下面小編給大家?guī)砹送暾a,感興趣的朋友跟隨小編一起看看吧2021-12-12golang 實(shí)現(xiàn)tcp轉(zhuǎn)發(fā)代理的方法
今天小編就為大家分享一篇golang 實(shí)現(xiàn)tcp轉(zhuǎn)發(fā)代理的方法,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-08-08Go net/http/pprof分析內(nèi)存泄露及解決過程
這篇文章主要介紹了Go net/http/pprof分析內(nèi)存泄露及解決過程,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04Go中RPC遠(yuǎn)程過程調(diào)用的實(shí)現(xiàn)
本文主要介紹了Go中RPC遠(yuǎn)程過程調(diào)用的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-07-07goland中導(dǎo)包報(bào)紅和go mod問題
這篇文章主要介紹了goland中導(dǎo)包報(bào)紅和go mod問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03Go 1.21新增的slices包中切片函數(shù)用法詳解
Go 1.21新增的 slices 包提供了很多和切片相關(guān)的函數(shù),可以用于任何類型的切片,本文通過代碼示例為大家介紹了部分切片函數(shù)的具體用法,感興趣的小伙伴可以了解一下2023-08-08