原文連接:Go語言如何實現可重入鎖?html
哈嘍,你們好,我是asong
。前幾天一個讀者問我如何使用Go
語言實現可重入鎖,忽然想到Go
語言中好像沒有這個概念,日常在業務開發中也沒有要用到可重入鎖的概念,一時懵住了。以前在寫java
的時候,就會使用到可重入鎖,然而寫了這麼久的Go
,卻沒有使用過,這是怎麼回事呢?這一篇文章就帶你來解密~
以前寫過java
的同窗對這個概念應該瞭如指掌,可重入鎖又稱爲遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入該線程的內層方法時會自動獲取鎖,不會由於以前已經獲取過還沒釋放而阻塞。美團技術團隊的一篇關於鎖的文章當中針對可重入鎖進行了舉例:java
假設如今有多個村民在水井排隊打水,有管理員正在看管這口水井,村民在打水時,管理員容許鎖和同一我的的多個水桶綁定,這我的用多個水桶打水時,第一個水桶和鎖綁定並打完水以後,第二個水桶也能夠直接和鎖綁定並開始打水,全部的水桶都打完水以後打水人才會將鎖還給管理員。這我的的全部打水流程都可以成功執行,後續等待的人也可以打到水。這就是可重入鎖。git
下圖摘自美團技術團隊分享的文章:github
若是是非可重入鎖,,此時管理員只容許鎖和同一我的的一個水桶綁定。第一個水桶和鎖綁定打完水以後並不會釋放鎖,致使第二個水桶不能和鎖綁定也沒法打水。當前線程出現死鎖,整個等待隊列中的全部線程都沒法被喚醒。面試
下圖依舊摘自美團技術團隊分享的文章:安全
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/...
Go
語言中沒有互斥鎖這問題的答案,我在:https://stackoverflow.com/que...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學習交流羣,歡迎各位大佬們踊躍入羣,咱們一塊兒學習交流。入羣方式:關注公衆號[Golang夢工廠]獲取。更多學習資料請到公衆號領取。
推薦往期文章: