Go 語言 sync-Cond 使用詳解
【導讀】go 語言的 sync.Cond 在什麼場景下使用?本文對 sync.Cond 的使用做了梳理。
爲等待 / 通知場景下的併發問題提供支持。Cond 通常應用於等待某個條件的一組 goroutine,等條件變爲 true 的時候,其中一個 goroutine 或者所有的 goroutine 都會被喚醒執行。
說點人話吧……
cond 分析
我們來看一下 Cond 提供的方法
func NewCond(l Locker) *Cond {} // 創建一個 cond
func (c *Cond) Wait() {} // 阻塞,等待喚醒
func (c *Cond) Signal() {} // 喚醒一個等待者
func (c *Cond) Broadcast() {} // 喚醒所有等待者
第一步:創建一個 cond
c := sync.NewCond(&sync.Mutex{})
第二步:將 goroutine 阻塞在 c 上
// 這裏會有坑,下文再討論
c.Wait()
第三步:喚醒
// 喚醒所有等待者
c.Broadcast()
// 喚醒一個等待者
c.Signal()
這裏再回過頭去看 cond 的作用,應該清晰了不少, 我們再結合一個例子來看一下。
場景是百米賽跑,10 個運動員,進場以後做熱身運動,運動員熱身完成後示意裁判,10 個運動員都熱身完成,裁判發令起跑。
func main() {
c := sync.NewCond(&sync.Mutex{})
var readyCnt int
for i := 0; i < 10; i++ {
go func(i int) {
// 模擬熱身
time.Sleep(time.Duration(rand.Int63n(10)) * time.Second)
// 熱身結束,加鎖更改等待條件
c.L.Lock()
readyCnt++
c.L.Unlock()
fmt.Printf("運動員#%d 已準備就緒\n", i)
c.Signal() // 示意裁判員
}(i)
}
c.L.Lock()
for readyCnt != 10 { // 每次 c.Signal() 都會喚醒一次,喚醒 10 次才能開始比賽
c.Wait() // c.Wait() 調用後,會阻塞在這裏,直到被喚醒
fmt.Printf("裁判員被喚醒一次\n")
}
c.L.Unlock()
fmt.Println("所有運動員都準備就緒。比賽開始,3,2,1, ......")
}
這裏你可能會說,使用 sync.WaitGroup{}
或 channel
也可以實現,甚至比 cond 的實現還要簡單,的確如此,這也從側面說明 cond
的應用場景少之又少。
sync.WaitGroup{}
或 channel
這種併發原語適用的情況時,等待者只有一個,如果等待者有多個,cond
比較擅長。
我們來改一下場景,假設有兩個裁判,一個發令裁判,一個計時裁判,看代碼實現:
func main() {
c := sync.NewCond(&sync.Mutex{})
var readyCnt int
for i := 0; i < 10; i++ {
go func(i int) {
// 模擬熱身
time.Sleep(time.Duration(rand.Int63n(10)) * time.Second)
// 熱身結束,加鎖更改等待條件
c.L.Lock()
readyCnt++
c.L.Unlock()
fmt.Printf("運動員#%d 已準備就緒\n", i)
c.Broadcast() // 示意所有裁判員
}(i)
}
var wg sync.WaitGroup
wg.Add(2)
for i:=0; i<2; i++ {
go func(i int) {
defer wg.Done()
c.L.Lock()
for readyCnt != 10 {
c.Wait()
fmt.Printf("裁判員 %d 被喚醒一次\n", i)
}
c.L.Unlock()
}(i)
}
wg.Wait()
fmt.Println("所有運動員都準備就緒。比賽開始,3,2,1, ......")
}
關於代碼裏的一些細節,我們有必要說明一下,readyCnt++
需要加鎖,這個很明顯。對於 c.Wait()
的操作,需要先獲取鎖,這是由它的實現來決定的。
// Wait atomically unlocks c.L and suspends execution
// of the calling goroutine. After later resuming execution,
// Wait locks c.L before returning. Unlike in other systems,
// Wait cannot return unless awoken by Broadcast or Signal.
//
// Because c.L is not locked when Wait first resumes, the caller
// typically cannot assume that the condition is true when
// Wait returns. Instead, the caller should Wait in a loop:
//
// c.L.Lock()
// for !condition() {
// c.Wait()
// }
// ... make use of condition ...
// c.L.Unlock()
//
func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify) // 加入到等待隊列
c.L.Unlock() // 解鎖
runtime_notifyListWait(&c.notify, t) // 阻塞等待直到被喚醒
c.L.Lock() // 加鎖
}
調用 Wait()
時,它會把當前 goroutine 放入等待隊列,然後解鎖,將自己阻塞等待喚醒,當有其它 goroutine 執行了喚醒操作時,會先獲取鎖,然後執行 Wait
後面的代碼。這裏需要注意的是,任何 goroutine 都能執行喚醒操作,但並不是每次喚醒都滿足了條件,比如說上述的 demo,每個運動員熱身完成後,都會示意裁判(執行一次喚醒),但是要等 10 個運動員都熱身完成後,比賽才能開始。所以官方的註釋裏給我們的建議是使用 for 能夠確保條件符合要求後,再執行後續的代碼
c.L.Lock()
for !condition() {
c.Wait()
}
... make use of condition ...
c.L.Unlock()
對應到我們的 demo 就是
for readyCnt != 10 {
c.Wait()
fmt.Printf("裁判員 %d 被喚醒一次\n", i)
}
c.L.Unlock()
那我們再反問下自己,Wait()
爲什麼要如此設計:解鎖在前,加鎖在後?我們來改一下 Wait()
func (c *Cond) Wait() {
c.L.Lock()
c.checker.check()
t := runtime_notifyListAdd(&c.notify) // 更新操作加鎖保護
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
}
撇開其它業務邏輯不談,這樣子是完全沒有問題的,需要併發安全的,我們加鎖保護來起來,runtime_notifyListWait(&c.notify, t)
是一個耗時的阻塞操作,不在鎖的保護區,也不會有性能問題。
這個時候我們再看外層的業務邏輯,condition 的檢查涉及到併發訪問資源的問題,我們需要加鎖對其保護,那就需要
var mutex sync.Mutex
mutex.Lock() // 加鎖訪問 condition
for !condition() {
mutex.Unlock() // 釋放掉鎖,防止其它 goroutine 阻塞
c.Wait() // 這個是業務上的阻塞操作,等待喚醒
mutex.Lock() // 到這裏時,被喚醒了,需要加鎖訪問 condition,進行 !condition 判斷
}
... make use of condition ...
mutex.Unlock()
我們把 c.Wait()
的代碼組合進來再看
var mutex sync.Mutex
mutex.Lock() // 加鎖訪問 condition
for !condition() {
mutex.Unlock() // 釋放掉鎖,防止其它 goroutine 阻塞
// c.Wait() 源碼
c.L.Lock()
c.checker.check()
t := runtime_notifyListAdd(&c.notify) // 更新操作加鎖保護
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
mutex.Lock() // 到這裏時,被喚醒了,需要加鎖訪問 condition,進行 !condition 判斷
}
... make use of condition ...
mutex.Unlock()
你會發現 mutex.Unlock 和 c.L.Lock 中間什麼也沒發生,那如果 mutex 和 c.L 是同一把鎖的話,這兩個操作可以直接去掉了。
事實是它們就是一把鎖,因爲 condition 就是和 這個 c 綁定的,那通過 c.L 來控制 condition 的併發訪問,是理所應當的。
把兩把鎖換成同一把,去掉多餘的代碼
c.L.Lock()
for !condition() {
// c.Wait() 源碼
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock().Lock()
}
... make use of condition ...
mutex.Unlock()
這不就變成了
func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
c.L.Lock()
for !condition() {
c.Wait()
}
... make use of condition ...
c.L.Unlock()
妙哉妙哉~~~
易錯分析
- 調用 Wait 前,必須先加鎖
錯誤寫法
for !condition() {
c.Wait()
}
... make use of condition ...
- 只調用了一次 Wait,沒有等到所有條件都滿足就返回了
錯誤寫法
c.L.Lock()
c.Wait()
... make use of condition ...
c.L.Unlock()
轉自:
zero-tt.fun/go/cond/
Go 開發大全
參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/AECG9FiydmELH9s_prWLaQ