Go語言底層原理互斥鎖的實(shí)現(xiàn)原理
Go 互斥鎖的實(shí)現(xiàn)原理?
Go sync包提供了兩種鎖類型:互斥鎖sync.Mutex 和 讀寫互斥鎖sync.RWMutex,都屬于悲觀鎖。
概念
Mutex是互斥鎖,當(dāng)一個(gè) goroutine 獲得了鎖后,其他 goroutine 不能獲取鎖(只能存在一個(gè)寫者或讀者,不能同時(shí)讀和寫)
使用場景
多個(gè)線程同時(shí)訪問臨界區(qū),為保證數(shù)據(jù)的安全,鎖住一些共享資源, 以防止并發(fā)訪問這些共享數(shù)據(jù)時(shí)可能導(dǎo)致的數(shù)據(jù)不一致問題。
獲取鎖的線程可以正常訪問臨界區(qū),未獲取到鎖的線程等待鎖釋放后可以嘗試獲取鎖

底層實(shí)現(xiàn)結(jié)構(gòu)
互斥鎖對應(yīng)的是底層結(jié)構(gòu)是sync.Mutex結(jié)構(gòu)體,,位于 src/sync/mutex.go中
type Mutex struct {
state int32
sema uint32
}state表示鎖的狀態(tài),有鎖定、被喚醒、饑餓模式等,并且是用state的二進(jìn)制位來標(biāo)識的,不同模式下會有不同的處理方式

sema表示信號量,mutex阻塞隊(duì)列的定位是通過這個(gè)變量來實(shí)現(xiàn)的,從而實(shí)現(xiàn)goroutine的阻塞和喚醒

addr = &sema
func semroot(addr *uint32) *semaRoot {
return &semtable[(uintptr(unsafe.Pointer(addr))>>3)%semTabSize].root
}
root := semroot(addr)
root.queue(addr, s, lifo)
root.dequeue(addr)
var semtable [251]struct {
root semaRoot
...
}
type semaRoot struct {
lock mutex
treap *sudog // root of balanced tree of unique waiters.
nwait uint32 // Number of waiters. Read w/o the lock.
}
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // 指向sema變量
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
...
}操作
鎖的實(shí)現(xiàn)一般會依賴于原子操作、信號量,通過atomic 包中的一些原子操作來實(shí)現(xiàn)鎖的鎖定,通過信號量來實(shí)現(xiàn)線程的阻塞與喚醒
加鎖
通過原子操作cas加鎖,如果加鎖不成功,根據(jù)不同的場景選擇自旋重試加鎖或者阻塞等待被喚醒后加鎖

func (m *Mutex) Lock() {
// Fast path: 幸運(yùn)之路,一下就獲取到了鎖
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// Slow path:緩慢之路,嘗試自旋或阻塞獲取鎖
m.lockSlow()
}解鎖
通過原子操作add解鎖,如果仍有g(shù)oroutine在等待,喚醒等待的goroutine

func (m *Mutex) Unlock() {
// Fast path: 幸運(yùn)之路,解鎖
new := atomic.AddInt32(&m.state, -mutexLocked)
if new != 0 {
// Slow path:如果有等待的goroutine,喚醒等待的goroutine
m.unlockSlow()
}
}注意點(diǎn):
- 在 Lock() 之前使用 Unlock() 會導(dǎo)致 panic 異常
- 使用 Lock() 加鎖后,再次 Lock() 會導(dǎo)致死鎖(不支持重入),需Unlock()解鎖后才能再加鎖
- 鎖定狀態(tài)與 goroutine 沒有關(guān)聯(lián),一個(gè) goroutine 可以 Lock,另一個(gè) goroutine 可以 Unlock
Go 互斥鎖正常模式和饑餓模式的區(qū)別?
在Go一共可以分為兩種搶鎖的模式,一種是正常模式,另外一種是饑餓模式。
正常模式(非公平鎖)
在剛開始的時(shí)候,是處于正常模式(Barging),也就是,當(dāng)一個(gè)G1持有著一個(gè)鎖的時(shí)候,G2會自旋的去嘗試獲取這個(gè)鎖
當(dāng)自旋超過4次還沒有能獲取到鎖的時(shí)候,這個(gè)G2就會被加入到獲取鎖的等待隊(duì)列里面,并阻塞等待喚醒
正常模式下,所有等待鎖的 goroutine 按照 FIFO(先進(jìn)先出)順序等待。喚醒的goroutine 不會直接擁有鎖,而是會和新請求鎖的 goroutine 競爭鎖。新請求鎖的 goroutine 具有優(yōu)勢:它正在 CPU 上執(zhí)行,而且可能有好幾個(gè),所以剛剛喚醒的 goroutine 有很大可能在鎖競爭中失敗,長時(shí)間獲取不到鎖,就會切換到饑餓模式
饑餓模式(公平鎖)
當(dāng)一個(gè) goroutine 等待鎖時(shí)間超過 1 毫秒時(shí),它可能會遇到饑餓問題。 在版本1.9中,這種場景下Go Mutex 切換到饑餓模式(handoff),解決饑餓問題。
starving = runtime_nanotime()-waitStartTime > 1e6
正常模式下,所有等待鎖的 goroutine 按照 FIFO(先進(jìn)先出)順序等待。喚醒的goroutine 不會直接擁有鎖,而是會和新請求鎖的 goroutine 競爭鎖。新請求鎖的 goroutine 具有優(yōu)勢:它正在 CPU 上執(zhí)行,而且可能有好幾個(gè),所以剛剛喚醒的 goroutine 有很大可能在鎖競爭中失敗,長時(shí)間獲取不到鎖,就會切換到饑餓模式
那么也不可能說永遠(yuǎn)的保持一個(gè)饑餓的狀態(tài),總歸會有吃飽的時(shí)候,也就是總有那么一刻Mutex會回歸到正常模式,那么回歸正常模式必須具備的條件有以下幾種:
- G的執(zhí)行時(shí)間小于1ms
- 等待隊(duì)列已經(jīng)全部清空了
當(dāng)滿足上述兩個(gè)條件的任意一個(gè)的時(shí)候,Mutex會切換回正常模式,而Go的搶鎖的過程,就是在這個(gè)正常模式和饑餓模式中來回切換進(jìn)行的。
delta := int32(mutexLocked - 1<<mutexWaiterShift)
if !starving || old>>mutexWaiterShift == 1 {
delta -= mutexStarving
}
atomic.AddInt32(&m.state, delta)小結(jié):
對于兩種模式,正常模式下的性能是最好的,goroutine 可以連續(xù)多次獲取鎖,饑餓模式解決了取鎖公平的問題,但是性能會下降,其實(shí)是性能和公平的 一個(gè)平衡模式。
Go 互斥鎖允許自旋的條件?
線程沒有獲取到鎖時(shí)常見有2種處理方式:
- 一種是沒有獲取到鎖的線程就一直循環(huán)等待判斷該資源是否已經(jīng)釋放鎖,這種鎖也叫做自旋鎖,它不用將線程阻塞起來, 適用于并發(fā)低且程序執(zhí)行時(shí)間短的場景,缺點(diǎn)是cpu占用較高
- 另外一種處理方式就是把自己阻塞起來,會釋放CPU給其他線程,內(nèi)核會將線程置為「睡眠」?fàn)顟B(tài),等到鎖被釋放后,內(nèi)核會在合適的時(shí)機(jī)喚醒該線程,適用于高并發(fā)場景,缺點(diǎn)是有線程上下文切換的開銷
Go語言中的Mutex實(shí)現(xiàn)了自旋與阻塞兩種場景,當(dāng)滿足不了自旋條件時(shí),就會進(jìn)入阻塞
允許自旋的條件:
- 鎖已被占用,并且鎖不處于饑餓模式。
- 積累的自旋次數(shù)小于最大自旋次數(shù)(active_spin=4)。
- cpu 核數(shù)大于 1。
- 有空閑的 P。
- 當(dāng)前 goroutine 所掛載的 P 下,本地待運(yùn)行隊(duì)列為空。
if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {
...
runtime_doSpin()
continue
}
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
}自旋:
func sync_runtime_doSpin() {
procyield(active_spin_cnt)
} 如果可以進(jìn)入自旋狀態(tài)之后就會調(diào)用 runtime_doSpin 方法進(jìn)入自旋, doSpin 方法會調(diào)用 procyield(30) 執(zhí)行30次 PAUSE 指令,什么都不做,但是會消耗CPU時(shí)間
到此這篇關(guān)于Go語言底層原理互斥鎖的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Go 互斥鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語言 channel如何實(shí)現(xiàn)歸并排序中的merge函數(shù)詳解
這篇文章主要給大家介紹了關(guān)于Go語言 channel如何實(shí)現(xiàn)歸并排序中merge函數(shù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-02-02
go語言中使用ent做關(guān)聯(lián)查詢的示例詳解
go語言的ent框架是facebook開源的ORM框架,是go語言開發(fā)中的常用框架,而關(guān)聯(lián)查詢又是日常開發(fā)中的常見數(shù)據(jù)庫操作,故文本給出一個(gè)使用ent做關(guān)聯(lián)查詢的使用示例,需要的朋友可以參考下2024-02-02
golang小游戲開發(fā)實(shí)戰(zhàn)之飛翔的小鳥
這篇文章主要給大家介紹了關(guān)于golang小游戲開發(fā)實(shí)戰(zhàn)之飛翔的小鳥的相關(guān)資料,,本文可以帶你你從零開始,一步一步的開發(fā)出這款小游戲,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-03-03
在Go語言中實(shí)現(xiàn)DDD領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)實(shí)例探究
本文將詳細(xì)探討在Go項(xiàng)目中實(shí)現(xiàn)DDD的核心概念、實(shí)踐方法和實(shí)例代碼,包括定義領(lǐng)域模型、創(chuàng)建倉庫、實(shí)現(xiàn)服務(wù)層和應(yīng)用層,旨在提供一份全面的Go DDD實(shí)施指南2024-01-01
gorm FirstOrCreate和受影響的行數(shù)實(shí)例
這篇文章主要介紹了gorm FirstOrCreate和受影響的行數(shù)實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Go語言優(yōu)雅實(shí)現(xiàn)單例模式的多種方式
單例模式(Singleton Pattern)是一種設(shè)計(jì)模式,旨在保證一個(gè)類只有一個(gè)實(shí)例,并且提供全局訪問點(diǎn),單例模式通常用于需要限制某個(gè)對象的實(shí)例數(shù)量為一個(gè)的場景,本文給大家介紹了Go語言實(shí)現(xiàn)單例模式的多種方式,需要的朋友可以參考下2025-02-02

