一文帶你了解Go語言中鎖特性和實現(xiàn)
鎖底層
go
中的sync
包提供了兩種鎖的類型,分別是互斥鎖sync.Mutex
和讀寫鎖sync.RWMutex
,這兩種鎖都屬于悲觀鎖
鎖的使用場景是解決多協(xié)程下數(shù)據(jù)競態(tài)的問題,為了保證數(shù)據(jù)的安全,鎖住一些共享資源。以防止并發(fā)訪問這些共享數(shù)據(jù)時可能導致的數(shù)據(jù)不一致問題,獲取鎖的線程可以正常訪問臨界區(qū),未獲取到鎖的線程等待鎖釋放之后可以嘗試獲取鎖
注:當你想讓一個結(jié)構(gòu)體是并發(fā)安全的,可以加一個鎖字段,比如channel就是這么做的,要注意的是,這個鎖字段必須小寫,不然調(diào)用方也可以進行l(wèi)ock和unlock操作,相當于你把鑰匙和鎖都交給了別人,鎖就失去了應有的作用
mutex
提供了三個方法
- Lock() 進行加鎖操作,在同一個goroutine中必須在鎖釋放之后才能進行再次上鎖,不然會panic
- Unlock() 進行解鎖操作,如果這個時候未加鎖會panic,mutex和goroutine不關聯(lián),也就是說對于mutex的加鎖解鎖操作可以發(fā)生在多個goroutine間
- tryLock() 嘗試獲取鎖,當鎖被其他goroutine占有,或者鎖處于饑餓模式,將立刻返回false,當鎖可用時嘗試獲取鎖,獲取失敗也返回false
實現(xiàn)如下
type Mutex struct { state int32 sema uint32 }
Mutex只有兩個字段
- state 表示當前互斥鎖的狀態(tài),復合型字段
- sema 信號量變量,用來控制等待goroutine的阻塞休眠和喚醒
state的不同位標識了不同的狀態(tài),以此實現(xiàn)了用最小的內(nèi)存來表示更多的意義
// 前三個字段標識了鎖的狀態(tài) 剩下的位來標識當前共有多少個goroutine在等待鎖 const ( mutexLocked = 1 << iota // 表示互斥鎖的鎖定狀態(tài) mutexWoken // 表示從正常模式被從喚醒 mutexStarving // 當前的互斥鎖進入饑餓狀態(tài) mutexWaiterShift = iota // 當前互斥鎖上等待者的數(shù)量 )
mutex的最開始實現(xiàn)只有正常模式,在正常模式下等待的線程按照先進先出的方式獲取鎖,但是新創(chuàng)建的goroutine會與剛被喚醒的goroutine競爭,導致剛被喚起的goroutine拿不到鎖,從而長期被阻塞。
因此Go在1.9
版本中引入了饑餓模式,當goroutine超過1ms沒有獲取鎖,那么就將當前的互斥鎖切換到饑餓模式,在該模式下,互斥鎖會直接交給等待隊列最前面的g,新的g在該狀態(tài)下既不能獲取鎖,也不會進入自旋狀態(tài),只會在隊列的末尾等待。如果一個g獲取了互斥鎖,并且它在隊列的末尾或者等待的時間少于1ms,那么就回到正常模式
加鎖
func (m *Mutex) Lock() { // 判斷當前鎖的狀態(tài),如果鎖是完全空閑的,即m.state為0,則對其加鎖,將m.state的值賦為1 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() } func (m *Mutex) lockSlow() { var waitStartTime int64 starving := false awoke := false iter := 0 old := m.state ........ }
- 通過CAS系統(tǒng)調(diào)用判斷當前鎖的狀態(tài),如果是空閑則m.state為0,這個時候?qū)ζ浼渔i,將m.state設為1
- 如果當前鎖已被占用,通過lockSlow方法嘗試自旋或者饑餓狀態(tài)下的競爭,等待鎖的釋放
lockSlow:
初始化五個字段
- waitStartTime 用來計算waiter的等待時間
- starving 饑餓模式標志,如果等待時間超過1ms,則為true
- awoke 協(xié)程是否喚醒,當g在自旋的時候,相當于CPU上已經(jīng)有正在等鎖的協(xié)程,為了避免mutex解鎖時再喚醒其他協(xié)程,自旋時要嘗試把mutex設為喚醒狀態(tài)
- iter 用來記錄協(xié)程的自旋次數(shù)
- old 記錄當前鎖的狀態(tài)
判斷自旋
for { // 判斷是否允許進入自旋 兩個條件,條件1是當前鎖不能處于饑餓狀態(tài) // 條件2是在runtime_canSpin內(nèi)實現(xiàn),其邏輯是在多核CPU運行,自旋的次數(shù)小于4 if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { // !awoke 判斷當前goroutine不是在喚醒狀態(tài) // old&mutexWoken == 0 表示沒有其他正在喚醒的goroutine // old>>mutexWaiterShift != 0 表示等待隊列中有正在等待的goroutine // atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) 嘗試將當前鎖的低2位的Woken狀態(tài)位設置為1,表示已被喚醒, 這是為了通知在解鎖Unlock()中不要再喚醒其他的waiter了 if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) { // 設置當前goroutine喚醒成功 awoke = true } // 進行自旋 runtime_doSpin() // 自旋次數(shù) iter++ // 記錄當前鎖的狀態(tài) old = m.state continue } } const active_spin_cnt = 30 func sync_runtime_doSpin() { procyield(active_spin_cnt) } // asm_amd64.s TEXT runtime·procyield(SB),NOSPLIT,$0-0 MOVL cycles+0(FP), AX again: PAUSE SUBL $1, AX JNZ again RET
進入自旋的原因:樂觀的認為當前正在持有鎖的g能在短時間內(nèi)歸還鎖,所以需要一些條件來判斷:到底能不能短時間歸還
條件如下
- 自旋的次數(shù)<=4
- cpu必須為多核
- gomaxprocs>1,最大被同時執(zhí)行的CPU數(shù)目大于1
- 當前機器上至少存在一個正在運行的P并且處理隊列為空
滿足條件之后進行循環(huán),次數(shù)為30次,也就是執(zhí)行30次PAUSE指令來占據(jù)CPU,進行自旋
解鎖
func (m *Mutex) Unlock() { // 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) } } func (m *Mutex) unlockSlow(new int32) { // 這里表示解鎖了一個沒有上鎖的鎖,則直接發(fā)生panic if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } // 正常模式的釋放鎖邏輯 if new&mutexStarving == 0 { old := new for { // 如果沒有等待者則直接返回即可 // 如果鎖處于加鎖的狀態(tài),表示已經(jīng)有goroutine獲取到了鎖,可以返回 // 如果鎖處于喚醒狀態(tài),這表明有等待的goroutine被喚醒了,不用嘗試獲取其他goroutine了 // 如果鎖處于饑餓模式,鎖之后會直接給等待隊頭goroutine if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { return } // 搶占喚醒標志位,這里是想要把鎖的狀態(tài)設置為被喚醒,然后waiter隊列-1 new = (old - 1<<mutexWaiterShift) | mutexWoken if atomic.CompareAndSwapInt32(&m.state, old, new) { // 搶占成功喚醒一個goroutine runtime_Semrelease(&m.sema, false, 1) return } // 執(zhí)行搶占不成功時重新更新一下狀態(tài)信息,下次for循環(huán)繼續(xù)處理 old = m.state } } else { // 饑餓模式釋放鎖邏輯,直接喚醒等待隊列goroutine runtime_Semrelease(&m.sema, true, 1) } } func (m *Mutex) unlockSlow(new int32) { // 這里表示解鎖了一個沒有上鎖的鎖,則直接發(fā)生panic if (new+mutexLocked)&mutexLocked == 0 { throw("sync: unlock of unlocked mutex") } // 正常模式的釋放鎖邏輯 if new&mutexStarving == 0 { old := new for { // 如果沒有等待者則直接返回即可 // 如果鎖處于加鎖的狀態(tài),表示已經(jīng)有goroutine獲取到了鎖,可以返回 // 如果鎖處于喚醒狀態(tài),這表明有等待的goroutine被喚醒了,不用嘗試獲取其他goroutine了 // 如果鎖處于饑餓模式,鎖之后會直接給等待隊頭goroutine if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 { return } // 搶占喚醒標志位,這里是想要把鎖的狀態(tài)設置為被喚醒,然后waiter隊列-1 new = (old - 1<<mutexWaiterShift) | mutexWoken if atomic.CompareAndSwapInt32(&m.state, old, new) { // 搶占成功喚醒一個goroutine runtime_Semrelease(&m.sema, false, 1) return } // 執(zhí)行搶占不成功時重新更新一下狀態(tài)信息,下次for循環(huán)繼續(xù)處理 old = m.state } } else { // 饑餓模式釋放鎖邏輯,直接喚醒等待隊列goroutine runtime_Semrelease(&m.sema, true, 1) } }
解鎖對于加鎖來說簡單很多,通過AddInt32
方法進行快速解鎖,將m.state低位置為0,然后判斷值,如果為0,那么就完全空閑了,結(jié)束解鎖。如果不為0說明當前鎖未被占用,不過有等待的g未被喚醒,需要進行一系列喚醒操作,喚醒判斷鎖的狀態(tài),然后進行具體的goroutine喚醒
非阻塞加鎖
func (m *Mutex) TryLock() bool { // 記錄當前狀態(tài) old := m.state // 處于加鎖狀態(tài)/饑餓狀態(tài)直接獲取鎖失敗 if old&(mutexLocked|mutexStarving) != 0 { return false } // 嘗試獲取鎖,獲取失敗直接獲取失敗 if !atomic.CompareAndSwapInt32(&m.state, old, old|mutexLocked) { return false } return true }
TryLock是Go 1.18
新加入的方法,不被鼓勵使用,主要是兩個判斷邏輯
- 判斷當前鎖的狀態(tài),如果鎖處于加鎖狀態(tài)或者饑餓狀態(tài)就直接獲取鎖失敗
- 嘗試獲取鎖,如果失敗則直接失敗。
以上就是一文帶你了解Go語言中鎖特性和實現(xiàn)的詳細內(nèi)容,更多關于Go鎖的資料請關注腳本之家其它相關文章!
相關文章
golang框架gin的日志處理和zap lumberjack日志使用方式
這篇文章主要介紹了golang框架gin的日志處理和zap lumberjack日志使用方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01從淺入深帶你掌握Golang數(shù)據(jù)結(jié)構(gòu)map
在?Go?語言中,map?是一種非常常見的數(shù)據(jù)類型,它可以用于快速地檢索數(shù)據(jù)。本篇文章將介紹?Go?語言中的?map,包括?map?的定義、初始化、操作和優(yōu)化,需要的可以參考一下2023-04-04超實用的Golang通道指南之輕松實現(xiàn)并發(fā)編程
Golang?中的通道是一種高效、安全、靈活的并發(fā)機制,用于在并發(fā)環(huán)境下實現(xiàn)數(shù)據(jù)的同步和傳遞。本文主要介紹了如何利用通道輕松實現(xiàn)并發(fā)編程,需要的可以參考一下2023-04-04Golang實現(xiàn)程序優(yōu)雅退出的方法詳解
項目開發(fā)過程中,隨著需求的迭代,代碼的發(fā)布會頻繁進行,在發(fā)布過程中,Golang如何讓程序做到優(yōu)雅的退出?本文就來詳細為大家講講2022-06-06