這一次,徹底搞懂 Go Cond

haohongfan發表於2021-04-23

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 的?

原始碼剖析

cond wait

cond signal

cond broadcast

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” 。

相關文章