Go 語言如何實現可重入鎖?
前言
哈嘍,大家好,我是
asong
。前幾天一個讀者問我如何使用Go
語言實現可重入鎖,突然想到Go
語言中好像沒有這個概念,平常在業務開發中也沒有要用到可重入鎖的概念,一時懵住了。之前在寫java
的時候,就會使用到可重入鎖,然而寫了這麼久的Go
,卻沒有使用過,這是怎麼回事呢?這一篇文章就帶你來解密~
什麼是可重入鎖
之前寫過java
的同學對這個概念應該瞭如指掌,可重入鎖又稱爲遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入該線程的內層方法時會自動獲取鎖,不會因爲之前已經獲取過還沒釋放而阻塞。美團技術團隊的一篇關於鎖的文章當中針對可重入鎖進行了舉例:
假設現在有多個村民在水井排隊打水,有管理員正在看管這口水井,村民在打水時,管理員允許鎖和同一個人的多個水桶綁定,這個人用多個水桶打水時,第一個水桶和鎖綁定並打完水之後,第二個水桶也可以直接和鎖綁定並開始打水,所有的水桶都打完水之後打水人才會將鎖還給管理員。這個人的所有打水流程都能夠成功執行,後續等待的人也能夠打到水。這就是可重入鎖。
下圖摘自美團技術團隊分享的文章:
如果是非可重入鎖,,此時管理員只允許鎖和同一個人的一個水桶綁定。第一個水桶和鎖綁定打完水之後並不會釋放鎖,導致第二個水桶不能和鎖綁定也無法打水。當前線程出現死鎖,整個等待隊列中的所有線程都無法被喚醒。
下圖依舊摘自美團技術團隊分享的文章:
用Go
實現可重入鎖
既然我們想自己實現一個可重入鎖,那我們就要了解java
中可重入鎖是如何實現的,查看了ReentrantLock
的源碼,大致實現思路如下:
ReentrantLock
繼承了父類AQS
,其父類AQS
中維護了一個同步狀態status
來計數重入次數,status
初始值爲0
,當線程嘗試獲取鎖時,可重入鎖先嚐試獲取並更新status
值,如果status == 0
表示沒有其他線程在執行同步代碼,則把status
置爲1
,當前線程開始執行。如果status != 0
,則判斷當前線程是否是獲取到這個鎖的線程,如果是的話執行status+1
,且當前線程可以再次獲取鎖。釋放鎖時,可重入鎖同樣先獲取當前status
的值,在當前線程是持有鎖的線程的前提下。如果status-1 == 0
,則表示當前線程所有重複獲取鎖的操作都已經執行完畢,然後該線程纔會真正釋放鎖。
總結一下實現一個可重入鎖需要這兩點:
-
記住持有鎖的線程
-
統計重入的次數
統計重入的次數很容易實現,接下來我們考慮一下怎麼實現記住持有鎖的線程?
我們都知道Go
語言最大的特色就是從語言層面支持併發,Goroutine
是Go
中最基本的執行單元,每一個Go
程序至少有一個Goroutine
,主程序也是一個Goroutine
,稱爲主Goroutine
,當程序啓動時,他會自動創建。每個Goroutine
也是有自己唯一的編號,這個編號只有在panic
場景下才會看到,Go語言
卻刻意沒有提供獲取該編號的接口,官方給出的原因是爲了避免濫用。但是我們還是通過一些特殊手段來獲取Goroutine ID
的,可以使用runtime.Stack
函數輸出當前棧幀信息,然後解析字符串獲取Goroutine ID
,具體代碼可以參考開源項目 - goid。
因爲go
語言中的Goroutine
有Goroutine ID
,那麼我們就可以通過這個來記住當前的線程,通過這個來判斷是否持有鎖,就可以了,因此我們可以定義如下結構體:
type ReentrantLock struct {
lock *sync.Mutex
cond *sync.Cond
recursion int32
host int64
}
其實就是包裝了Mutex
鎖,使用host
字段記錄當前持有鎖的goroutine id
,使用recursion
字段記錄當前goroutine
的重入次數。這裏有一個特別要說明的就是sync.Cond
,使用Cond
的目的是,當多個Goroutine
使用相同的可重入鎖時,通過cond
可以對多個協程進行協調,如果有其他協程正在佔用鎖,則當前協程進行阻塞,直到其他協程調用釋放鎖。具體sync.Cond
的使用大家可以參考我之前的一篇文章:源碼剖析 sync.cond(條件變量的實現機制)。
- 構造函數
func NewReentrantLock() sync.Locker{
res := &ReentrantLock{
lock: new(sync.Mutex),
recursion: 0,
host: 0,
}
res.cond = sync.NewCond(res.lock)
return res
}
Lock
func (rt *ReentrantLock) Lock() {
id := GetGoroutineID()
rt.lock.Lock()
defer rt.lock.Unlock()
if rt.host == id{
rt.recursion++
return
}
for rt.recursion != 0{
rt.cond.Wait()
}
rt.host = id
rt.recursion = 1
}
這裏邏輯比較簡單,大概解釋一下:
首先我們獲取當前Goroutine
的ID
,然後我們添加互斥鎖鎖住當前代碼塊,保證併發安全,如果當前Goroutine
正在佔用鎖,則增加resutsion
的值,記錄當前線程加鎖的數量,然後返回即可。如果當前Goroutine
沒有佔用鎖,則判斷當前可重入鎖是否被其他Goroutine
佔用,如果有其他Goroutine
正在佔用可重入鎖,則調用cond.wait
方法進行阻塞,直到其他協程釋放鎖。
Unlock
func (rt *ReentrantLock) Unlock() {
rt.lock.Lock()
defer rt.lock.Unlock()
if rt.recursion == 0 || rt.host != GetGoroutineID() {
panic(fmt.Sprintf("the wrong call host: (%d); current_id: %d; recursion: %d", rt.host,GetGoroutineID(),rt.recursion))
}
rt.recursion--
if rt.recursion == 0{
rt.cond.Signal()
}
}
大概解釋如下:
首先我們添加互斥鎖鎖住當前代碼塊,保證併發安全,釋放可重入鎖時,如果非持有鎖的Goroutine
釋放鎖則會導致程序出現panic
,這個一般是由於用戶用法錯誤導致的。如果當前Goroutine
釋放了鎖,則調用cond.Signal
喚醒其他協程。
測試例子就不在這裏貼了,代碼已上傳github
:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/reentrantLock,歡迎 star。
爲什麼Go
語言中沒有可重入鎖
這問題的答案,我在:https://stackoverflow.com/questions/14670979/recursive-locking-in-go#14671462,這裏找到了答案。Go
語言的發明者認爲,如果當你的代碼需要重入鎖時,那就說明你的代碼有問題了,我們正常寫代碼時,從入口函數開始,執行的層次都是一層層往下的,如果有一個鎖需要共享給幾個函數,那麼就在調用這幾個函數的上面,直接加上互斥鎖就好了,不需要在每一個函數里面都添加鎖,再去釋放鎖。
舉個例子,假設我們現在一段這樣的代碼:
func F() {
mu.Lock()
//... do some stuff ...
G()
//... do some more stuff ...
mu.Unlock()
}
func G() {
mu.Lock()
//... do some stuff ...
mu.Unlock()
}
函數F()
和G()
使用了相同的互斥鎖,並且都在各自函數內部進行了加鎖,這要使用就會出現死鎖,使用可重入鎖可以解決這個問題,但是更好的方法是改變我們的代碼結構,我們進行分解代碼,如下:
func call(){
F()
G()
}
func F() {
mu.Lock()
... do some stuff
mu.Unlock()
}
func g() {
... do some stuff ...
}
func G() {
mu.Lock()
g()
mu.Unlock()
}
這樣不僅避免了死鎖,而且還對代碼進行了解耦。這樣的代碼按照作用範圍進行了分層,就像金字塔一樣,上層調用下層的函數,越往上作用範圍越大;各層有自己的鎖。
總結:Go
語言中完全沒有必要使用可重入鎖,如果我們發現我們的代碼要使用到可重入鎖了,那一定是我們寫的代碼有問題了,請檢查代碼結構,修改他!!!
總結
這篇文章我們知道了什麼是可重入鎖,並用Go
語言實現了可重入鎖,大家只需要知道這個概念就好了,實際開發中根本不需要。最後還是建議大家沒事多思考一下自己的代碼結構,好的代碼都是經過深思熟慮的,最後希望大家都能寫出漂亮的代碼。
好啦,這篇文章到此結束啦,素質三連(分享、點贊、在看)都是筆者持續創作更多優質內容的動力!我是asong
,我們下期見。
創建了一個 Golang 學習交流羣,歡迎各位大佬們踊躍入羣,我們一起學習交流。入羣方式:關注公衆號獲取。更多學習資料請到公衆號領取。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wBp4k7pJLNeSzyLVhGHLEA