Go語言如何實現可重入鎖?

原文連接: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語言最大的特點就是從語言層面支持併發,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
}
  • 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
}

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

首先咱們獲取當前GoroutineID,而後咱們添加互斥鎖鎖住當前代碼塊,保證併發安全,若是當前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夢工廠]獲取。更多學習資料請到公衆號領取。

推薦往期文章:

相關文章
相關標籤/搜索