這一次,徹底搞懂 Go Con

hi,大家好,我是 haohongfan。

本篇文章會從源碼角度去深入剖析下 sync.Cond。Go 日常開發中 sync.Cond 可能是我們用的較少的控制併發的手段,因爲大部分場景下都被 Channel 代替了。還有就是 sync.Cond 使用確實也蠻複雜的。

比如下面這段代碼:

package main

import (
 "fmt"
 "time"
)

func main() {
 done := make(chan int, 1)

 go func() {
  time.Sleep(5 * time.Second)
  done <- 1
 }()

 fmt.Println("waiting")
 <-done
 fmt.Println("done")
}

同樣可以使用 sync.Cond 來實現

package main

import (
 "fmt"
 "sync"
 "time"
)

func main() {
 cond := sync.NewCond(&sync.Mutex{})
 var flag bool
 go func() {
  time.Sleep(time.Second * 5)
  cond.L.Lock()
  flag = true
  cond.Signal()
  cond.L.Unlock()
 }()

 fmt.Println("waiting")
 cond.L.Lock()
 for !flag {
  cond.Wait()
 }
 cond.L.Unlock()
 fmt.Println("done")
}

大部分場景下使用 channel 是比 sync.Cond 方便的。不過我們要注意到,sync.Cond 提供了 Broadcast 方法,可以通知所有的等待者。想利用 channel 實現這個方法還是不容易的。我想這應該是 sync.Cond 唯一有用武之地的地方。

先列出來一些問題吧,可以帶着這些問題來閱讀本文:

  1. cond.Wait 本身就是阻塞狀態,爲什麼 cond.Wait 需要在循環內 ?

  2. sync.Cond 如何觸發不能複製的 panic ?

  3. 爲什麼 sync.Cond 不能被複制 ?

  4. cond.Signal 是如何通知一個等待的 goroutine ?

  5. cond.Broadcast 是如何通知等待的 goroutine 的?

源碼剖析

sync.cond wait

sync.Cond Signal

sync.Cond Broadcast

sync.Cond 排隊動圖

cond.Wait 是阻塞的嗎?是如何阻塞的?

是阻塞的。不過不是 sleep 這樣阻塞的。

調用 goparkunlock 解除當前 goroutine 的 m 的綁定關係,將當前 goroutine 狀態機切換爲等待狀態。等待後續 goready 函數時候能夠恢復現場。

cond.Signal 是如何通知一個等待的 goroutine ?

  1. 判斷是否有沒有被喚醒的 goroutine,如果都已經喚醒了,直接就返回了

  2. 將已通知 goroutine 的數量加 1

  3. 從等待喚醒的 goroutine 隊列中,獲取 head 指針指向的 goroutine,將其重新加入調度

  4. 被阻塞的 goroutine 可以繼續執行

cond.Broadcast 是如何通知等待的 goroutine 的?

  1. 判斷是否有沒有被喚醒的 goroutine,如果都已經喚醒了,直接就返回了

  2. 將等待通知的 goroutine 數量和已經通知過的 goroutine 數量設置成相等

  3. 遍歷等待喚醒的 goroutine 隊列,將所有的等待的 goroutine 都重新加入調度

  4. 所有被阻塞的 goroutine 可以繼續執行

cond.Wait 本身就是阻塞狀態,爲什麼 cond.Wait 需要在循環內 ?

我們能注意到,調用 cond.Wait 的位置,使用的是 for 的方式來調用 wait 函數,而不是使用 if 語句。

這是由於 wait 函數被喚醒時,存在虛假喚醒等情況,導致喚醒後發現,條件依舊不成立。因此需要使用 for 語句來循環地進行等待,直到條件成立爲止。

使用中注意點

1. 不能不加鎖直接調用 cond.Wait

func (c *Cond) Wait() {
 c.checker.check()
 t := runtime_notifyListAdd(&c.notify)
 c.L.Unlock()
 runtime_notifyListWait(&c.notify, t)
 c.L.Lock()
}

我們看到 Wait 內部會先調用 c.L.Unlock(),來先釋放鎖。如果調用方不先加鎖的話,會觸發 “fatal error: sync: unlock of unlocked mutex”。關於 mutex 的使用方法,推薦閱讀下《這可能是最容易理解的 Go Mutex 源碼剖析》

2. 爲什麼不能 sync.Cond 不能複製 ?

sync.Cond 不能被複制的原因,並不是因爲 sync.Cond 內部嵌套了 Locker。因爲 NewCond 時傳入的 Mutex/RWMutex 指針,對於 Mutex 指針複製是沒有問題的。

主要原因是 sync.Cond 內部是維護着一個 notifyList。如果這個隊列被複制的話,那麼就在併發場景下導致不同 goroutine 之間操作的 notifyList.wait、notifyList.notify 並不是同一個,這會導致出現有些 goroutine 會一直堵塞。

這裏留下一個問題,sync.Cond 內部是有一段代碼 check sync.Cond 是不能被複制的,下面這段代碼能觸發這個 panic 嗎?

package main

import (
 "fmt"
 "sync"
)

func main() {
 cond1 := sync.NewCond(new(sync.Mutex))
 cond := *cond1
 fmt.Println(cond)
}

有興趣的可以動手嘗試下,以及嘗試下如何才能觸發這個 panic "sync.Cond is copied” 。

sync.Cond 的剖析到這裏基本就結束了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2NsAl5yEPxwbpyATpuEa3Q