首頁 > 軟體

Go並行同步Mutex典型易錯使用場景

2022-08-02 14:05:10

Mutex的4種易錯使用場景

1.Lock/Unlock 不成對出現

Lock/Unlock 沒有成對出現,就可能會出現死鎖或者是因為Unlock一個未加鎖的Mutex而導致 panic。

忘記Unlock的情形

  • 程式碼中有太多的 if-else 分支,可能在某個分支中漏寫了 Unlock;
  • 在重構的時候把 Unlock 給刪除了;
  • Unlock 誤寫成了 Lock。

忘記Lock的情形一般是誤刪除了或者註釋掉了Lock。

eg:

func main() {
   var mu sync.Mutex
   defer mu.Unlock()
   fmt.Println("oh, missing Lock!")
}

error result:

2.Copy 已使用的 Mutex

實際上sync包下的同步原語在使用後都是不可複製的,原因在於Mutex是有狀態的,其state的值時刻在變化,如果複製一個已經加鎖的Metux物件給一個新的變數,可能這個變數剛初始化就顯示被加鎖了,這顯然是不合理的。

eg:以下程式碼在呼叫 foo 函數的時候,呼叫者會複製 Mutex 變數 c 作為 foo 函數的引數,不幸的是,複製之前已經使用了這個鎖,這就導致,複製的 Counter 是一個帶狀態 Counter,從而會導致死鎖。

type Counter struct {
   sync.Mutex
   Count int
}
func main() {
   var c Counter
   c.Lock()
   defer c.Unlock()
   c.Count++
   foo(c) // 複製鎖
}
// 這裡Counter的引數是通過複製的方式傳入的
func foo(c Counter) {
   c.Lock()
   defer c.Unlock()
   fmt.Println("in foo")
}

error result:還好有Go的協程死鎖檢查機制,程式執行後會快速失敗而不是一直hang住。

Go Vet指令

我們當然不想程式執行了才發現死鎖,我們可以通過go vet指令來在執行前檢查我們的程式碼是否存在lock copy問題:

檢查原理

檢查是通過copylock分析器靜態分析實現的。這個分析器會分析函數呼叫、range 遍歷、複製、宣告、函數返回值等位置,有沒有鎖的值 copy 的情景,以此來判斷有沒有問題。

通過原始碼我們可以看到實現了Lock或者Unlock介面的struct都支援copylock檢查。

var lockerType *types.Interface
 // Construct a sync.Locker interface type.
 func init() {
     nullary := types.NewSignature(nil, nil, nil, false) // func()
     methods := []*types.Func{
     types.NewFunc(token.NoPos, nil, "Lock", nullary),
     types.NewFunc(token.NoPos, nil, "Unlock", nullary),
 }
 lockerType = types.NewInterface(methods, nil).Complete()
 }

3.重入

Mutex不像Java中的ReentrantLock擁有可重入的功能,主要是因為其實現中沒有標記位記錄哪個goroutine 擁有這把鎖,所以Mutex是一個不可重入鎖,而一旦誤用Mutex的重入就會報錯。

eg:

func foo(l sync.Locker) {
   fmt.Println("in foo")
   l.Lock()
   bar(l)
   l.Unlock()
}
func bar(l sync.Locker) {
   l.Lock()
   fmt.Println("in bar")
   l.Unlock()
}
func main() {
   l := &sync.Mutex{}
   foo(l)
}

error result:我們可以看到當在bar方法中嘗試再次獲取鎖時,獲取不到,觸發了死鎖。

4.死鎖

兩個或兩個以上的程序(或執行緒,goroutine)

執行過程中,因爭奪共用資源而處於一種互相等待的狀態,如果沒有外部干涉,它們都將無法推進下去,此時,我們稱系統處於死鎖狀態或系統產生了死鎖。

死鎖產生的4個必要條件

如果想避免死鎖,我們只要思考如何打破以下任意條件就可以。

  • 1.互斥: 至少一個資源是被排他性獨享的,其他執行緒必須處於等待狀態,直到資源被釋放。
  • 2.持有和等待:goroutine 持有一個資源,並且還在請求其它 goroutine 持有的資源,也就是咱們常說的“吃著碗裡,看著鍋裡”的意思。
  • 3. 不可剝奪:資源只能由持有它的 goroutine 來釋放。
  • 4.環路等待:一般來說,存在一組等待程序,P={P1,P2,…,PN},P1 等待 P2 持有的

資源,P2 等待 P3 持有的資源,依此類推,最後是 PN 等待 P1 持有的資源,這就形成
了一個環路等待的死結。

eg:在這裡我們以辦理居住證業務,舉一個簡單的環路等待導致死鎖的例子:

//辦理居住證
func main() {
   // 網籤中心證明
   var psCertificate sync.Mutex
   // 社群證明
   var propertyCertificate sync.Mutex
   var wg sync.WaitGroup
   wg.Add(2) // 需要網籤中心和社群都處理
   // 網籤中心處理goroutine
   go func() {
      defer wg.Done() // 網籤中心處理完成
      psCertificate.Lock()
      defer psCertificate.Unlock()
      // 檢查材料
      time.Sleep(5 * time.Second)
      // 請求社群的證明
      propertyCertificate.Lock()
      propertyCertificate.Unlock()
   }()
   // 社群處理goroutine
   go func() {
      defer wg.Done() // 社群處理完成
      propertyCertificate.Lock()
      defer propertyCertificate.Unlock()
      // 檢查材料
      time.Sleep(5 * time.Second)
      // 請求網籤中心的證明
      psCertificate.Lock()
      psCertificate.Unlock()
   }()
   wg.Wait()
   fmt.Println("成功完成")
}

error result:

解決策略

1.可以引入一個第三方的鎖,大家都依賴這個鎖進行業務處理,比如現在政府推行的一站式政務服務中心。

2.解決持有等待問題,比如社群不需要看到網籤中心的證明才給開居住證明。

以上就是Go並行同步Mutex典型易錯使用場景的詳細內容,更多關於Go Mutex易錯場景的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com