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語言最大的特色就是從語言層面支持併發,GoroutineGo中最基本的執行單元,每一個Go程序至少有一個Goroutine,主程序也是一個Goroutine,稱爲主Goroutine,當程序啓動時,他會自動創建。每個Goroutine也是有自己唯一的編號,這個編號只有在panic場景下才會看到,Go語言卻刻意沒有提供獲取該編號的接口,官方給出的原因是爲了避免濫用。但是我們還是通過一些特殊手段來獲取Goroutine ID的,可以使用runtime.Stack函數輸出當前棧幀信息,然後解析字符串獲取Goroutine ID,具體代碼可以參考開源項目 - goid。

因爲go語言中的GoroutineGoroutine 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
}
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
}

這裏邏輯比較簡單,大概解釋一下:

首先我們獲取當前GoroutineID,然後我們添加互斥鎖鎖住當前代碼塊,保證併發安全,如果當前Goroutine正在佔用鎖,則增加resutsion的值,記錄當前線程加鎖的數量,然後返回即可。如果當前Goroutine沒有佔用鎖,則判斷當前可重入鎖是否被其他Goroutine佔用,如果有其他Goroutine正在佔用可重入鎖,則調用cond.wait方法進行阻塞,直到其他協程釋放鎖。

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