詳解go語(yǔ)言中并發(fā)安全和鎖問(wèn)題
首先可以先看看這篇文章,對(duì)鎖有些了解
Mutex-互斥鎖
Mutex 的實(shí)現(xiàn)主要借助了 CAS 指令 + 自旋 + 信號(hào)量
數(shù)據(jù)結(jié)構(gòu):
type Mutex struct {
state int32
sema uint32
}
上述兩個(gè)加起來(lái)只占 8 字節(jié)空間的結(jié)構(gòu)體表示了 Go語(yǔ)言中的互斥鎖
狀態(tài):
在默認(rèn)情況下,互斥鎖的所有狀態(tài)位都是 0,int32 中的不同位分別表示了不同的狀態(tài):
- 1位表示是否被鎖定
- 1位表示是否有協(xié)程已經(jīng)被喚醒
- 1位表示是否處于饑餓狀態(tài)
- 剩下29位表示阻塞的協(xié)程數(shù)
正常模式和饑餓模式
正常模式:所有g(shù)oroutine按照FIFO的順序進(jìn)行鎖獲取,被喚醒的goroutine和新請(qǐng)求鎖的goroutine同時(shí)進(jìn)行鎖獲取,通常新請(qǐng)求鎖的goroutine更容易獲取鎖(持續(xù)占有cpu),被喚醒的goroutine則不容易獲取到鎖
饑餓模式:所有嘗試獲取鎖的goroutine進(jìn)行等待排隊(duì),新請(qǐng)求鎖的goroutine不會(huì)進(jìn)行鎖獲取(禁用自旋),而是加入隊(duì)列尾部等待獲取鎖
如果一個(gè) Goroutine 獲得了互斥鎖并且它在隊(duì)列的末尾或者它等待的時(shí)間少于 1ms,那么當(dāng)前的互斥鎖就會(huì)切換回正常模式。
與饑餓模式相比,正常模式下的互斥鎖能夠提供更好地性能,饑餓模式的能避免 Goroutine 由于陷入等待無(wú)法獲取鎖而造成的高尾延時(shí)。
互斥鎖加鎖過(guò)程
- 如果互斥鎖處于初始狀態(tài),會(huì)直接加鎖
- 如果互斥鎖處于加鎖狀態(tài),并且工作在普通模式下,goroutine會(huì)進(jìn)入自旋,等待鎖的釋放
goroutine 進(jìn)入自旋的條件非??量蹋?/p>
- 互斥鎖只有在普通模式才能進(jìn)入自旋;
runtime.sync_runtime_canSpin需要返回 true運(yùn)行在多 CPU 的機(jī)器上;
當(dāng)前 Goroutine 為了獲取該鎖進(jìn)入自旋的次數(shù)小于四次;
當(dāng)前機(jī)器上至少存在一個(gè)正在運(yùn)行的處理器 P 并且處理的運(yùn)行隊(duì)列為空;
- 如果當(dāng)前 Goroutine 等待鎖的時(shí)間超過(guò)了 1ms,互斥鎖就會(huì)切換到饑餓模式;
- 互斥鎖在正常情況下會(huì)通
runtime.sync_runtime_SemacquireMutex將嘗試獲取鎖的 Goroutine 切換至休眠狀態(tài),等待鎖的持有者喚醒; - 如果當(dāng)前 Goroutine 是互斥鎖上的最后一個(gè)等待的協(xié)程或者等待的時(shí)間小于 1ms,那么它會(huì)將互斥鎖切換回正常模式;
互斥鎖解鎖過(guò)程
當(dāng)互斥鎖已經(jīng)被解鎖時(shí),再解鎖會(huì)拋出異常
當(dāng)互斥鎖處于饑餓模式時(shí),將鎖的所有權(quán)交給等待隊(duì)列最前面的 Goroutine
當(dāng)互斥鎖處于正常模式時(shí),如果沒(méi)有 Goroutine 等待鎖的釋放或者已經(jīng)有被喚醒的 Goroutine 獲得了鎖,會(huì)直接返回;在其他情況下會(huì)通過(guò)喚醒對(duì)應(yīng)的 Goroutine;
關(guān)于互斥鎖鎖的使用建議寫業(yè)務(wù)時(shí)不能全局使用同一個(gè) Mutex千萬(wàn)不要將要加鎖和解鎖分到兩個(gè)以上 Goroutine 中進(jìn)行Mutex 千萬(wàn)不能被復(fù)制(包括不能通過(guò)函數(shù)參數(shù)傳遞),否則會(huì)復(fù)制傳參前鎖的狀態(tài):已鎖定 or 未鎖定。很容易產(chǎn)生死鎖,關(guān)鍵是編譯器還發(fā)現(xiàn)不了這個(gè) Deadlock~
RWMutex-讀寫鎖
Go 中 RWMutex 使用的是寫優(yōu)先的設(shè)計(jì)
數(shù)據(jù)結(jié)構(gòu):
type RWMutex struct {
w Mutex //復(fù)用互斥鎖提供的能力
writerSem uint32 //writer信號(hào)量
readerSem uint32 //reader信號(hào)量
readerCount int32 //存儲(chǔ)了當(dāng)前正在執(zhí)行的讀操作數(shù)量
readerWait int32 // 表示寫操作阻塞時(shí),等待讀操作完成的個(gè)數(shù)
}
寫鎖
獲取寫鎖 :
- 調(diào)用結(jié)構(gòu)體持有的Mutex結(jié)構(gòu)體的Mutex.Lock阻塞后續(xù)的寫操作
- 將
readerCount減少2^30,成為負(fù)數(shù),以阻塞后續(xù)讀操作 - 如果有其他Goroutine 持有讀鎖,該 Goroutine會(huì)進(jìn)入休眠狀態(tài)等待所有讀鎖執(zhí)行結(jié)束后釋放
writerSem信號(hào)量將當(dāng)前協(xié)程喚醒
釋放寫鎖:
- 將
readerCount變回正數(shù),釋放讀鎖 - 喚醒所有因?yàn)樽x鎖而睡眠的Goroutine
- 調(diào)用Mutex.Unlock 釋放寫鎖
獲取寫鎖時(shí)會(huì)先阻塞寫鎖的獲取,后阻塞讀鎖的獲取,這種策略能夠保證讀操作不會(huì)被連續(xù)的寫操作『餓死』。
讀鎖
獲取讀鎖
獲取讀鎖的方法 sync.RWMutex.RLock 很簡(jiǎn)單,該方法會(huì)將readerCount加一:
- 如果該方法返回負(fù)數(shù)(代表其他 goroutine 獲得了寫鎖,當(dāng)前 goroutine 就會(huì)使其陷入休眠等待鎖的釋放
- 如果該方法返回結(jié)果為非負(fù)數(shù),代表沒(méi)有 goroutine 獲得寫鎖,會(huì)成功返回
釋放讀鎖
解鎖讀鎖的方法sync.RWMutex.RUnlock,該方法會(huì):
- 將
readerCount減一,根據(jù)返回值的不同會(huì)分別進(jìn)行處理 - 如果返回值大于等于0,讀鎖直接解鎖成功
- 如果小于0代表有正在執(zhí)行的寫操作,會(huì)調(diào)用
sync.RWMutex.rUnlockSlow,將readerWait減一,并且當(dāng)所有讀操作都被釋放后觸發(fā)信號(hào)量writerSem,該信號(hào)量被觸發(fā)時(shí),調(diào)度器就會(huì)喚醒嘗試獲取寫鎖的 Goroutine
WaitGroup
sync.WaitGroup可以等待一組 Goroutine 的返回
sync.WaitGroup 對(duì)外暴露了三個(gè)方法:
| 方法名 | 功能 |
|---|---|
| (wg * WaitGroup) Add(delta int) | 計(jì)數(shù)器+delta |
| (wg *WaitGroup) Done() | 計(jì)數(shù)器減1 |
| (wg *WaitGroup) Wait() | 阻塞直到計(jì)數(shù)器變?yōu)? |
sync.WaitGroup.Done只是對(duì) sync.WaitGroup.Add 方法的簡(jiǎn)單封裝,相當(dāng)于是加 -1
Sync.Map
Go語(yǔ)言中內(nèi)置的map不是并發(fā)安全的。
Go語(yǔ)言的sync包中提供了一個(gè)開(kāi)箱即用的并發(fā)安全版map–sync.Map。使用互斥鎖保證并發(fā)安全
數(shù)據(jù)結(jié)構(gòu):
type Map struct {
mu Mutex
read atomic.Value // readOnly
dirty map[interface{}]*entry
misses int
}
開(kāi)箱即用表示不用像內(nèi)置的map一樣使用make函數(shù)初始化就能直接使用。同時(shí)sync.Map內(nèi)置了方法:
| 方法名 | 功能 |
|---|---|
| (m *sync.Map)Store(key, value interface{}) | 保存鍵值對(duì) |
| (m *sync.Map)Load(key interface{}) | 根據(jù)key獲取對(duì)應(yīng)的值 |
| (m *sync.Map)Delete(key interface{}) | 刪除鍵值對(duì) |
| (m *sync.Map)Range(f func(key, value interface{}) bool) | 遍歷 sync.Map。Range 的參數(shù)是一個(gè)函數(shù) |
原子操作(atomic包)
代碼中的加鎖操作因?yàn)樯婕皟?nèi)核態(tài)的上下文切換會(huì)比較耗時(shí)、代價(jià)比較高。針對(duì)基本數(shù)據(jù)類型我們還可以使用原子操作來(lái)保證并發(fā)安全,因?yàn)樵硬僮魇荊o語(yǔ)言提供的方法它在用戶態(tài)就可以完成,因此性能比加鎖操作更好。Go語(yǔ)言中原子操作由內(nèi)置的標(biāo)準(zhǔn)庫(kù)sync/atomic提供。
參考資料:
Go 語(yǔ)言并發(fā)編程、同步原語(yǔ)與鎖 | Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn) (draveness.me)
到此這篇關(guān)于go語(yǔ)言中并發(fā)安全和鎖的文章就介紹到這了,更多相關(guān)go語(yǔ)言中并發(fā)安全和鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang?channel為什么不會(huì)阻塞的原因詳解
這篇文章主要為大家介紹了Golang?channel為什么不會(huì)阻塞的原因詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
Golang異常處理之defer,panic,recover的使用詳解
這篇文章主要為大家介紹了Go語(yǔ)言異常處理機(jī)制中defer、panic和recover三者的使用方法,文中示例代碼講解詳細(xì),需要的朋友可以參考下2022-05-05
Golang定時(shí)器Timer與Ticker的使用詳解
在 Go 里有很多種定時(shí)器的使用方法,像常規(guī)的 Timer、Ticker 對(duì)象,本文主要為大家介紹了Timer與Ticker的使用,感興趣的小伙伴可以了解一下2023-05-05
Go?CSV包實(shí)現(xiàn)結(jié)構(gòu)體和csv內(nèi)容互轉(zhuǎn)工具詳解
這篇文章主要介紹了Go?CSV包實(shí)現(xiàn)結(jié)構(gòu)體和csv內(nèi)容互轉(zhuǎn)工具詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
Golang 利用反射對(duì)結(jié)構(gòu)體優(yōu)雅排序的操作方法
這篇文章主要介紹了Golang 利用反射對(duì)結(jié)構(gòu)體優(yōu)雅排序的操作方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-10-10
golang 內(nèi)存對(duì)齊的實(shí)現(xiàn)
在代碼編譯階段,編譯器會(huì)對(duì)數(shù)據(jù)的存儲(chǔ)布局進(jìn)行對(duì)齊優(yōu)化,本文主要介紹了golang 內(nèi)存對(duì)齊的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-08-08
Go語(yǔ)言排序算法之插入排序與生成隨機(jī)數(shù)詳解
從這篇文章開(kāi)始將帶領(lǐng)大家學(xué)習(xí)Go語(yǔ)言的經(jīng)典排序算法,比如插入排序、選擇排序、冒泡排序、希爾排序、歸并排序、堆排序和快排,二分搜索,外部排序和MapReduce等,本文將先詳細(xì)介紹插入排序,并給大家分享了go語(yǔ)言生成隨機(jī)數(shù)的方法,下面來(lái)一起看看吧。2017-11-11

