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 唯一有用武之地的地方。
先列出來一些問題吧,可以帶著這些問題來閱讀本文:
- cond.Wait本身就是阻塞狀態,為什麼 cond.Wait 需要在迴圈內 ?
- sync.Cond 如何觸發不能複製的 panic ?
- 為什麼 sync.Cond 不能被複制 ?
- cond.Signal 是如何通知一個等待的 goroutine ?
- cond.Broadcast 是如何通知等待的 goroutine 的?
原始碼剖析
cond.Wait 是阻塞的嗎?是如何阻塞的?
是阻塞的。不過不是 sleep 這樣阻塞的。
呼叫 goparkunlock
解除當前 goroutine 的 m 的繫結關係,將當前 goroutine 狀態機切換為等待狀態。等待後續 goready 函式時候能夠恢復現場。
cond.Signal 是如何通知一個等待的 goroutine ?
- 判斷是否有沒有被喚醒的 goroutine,如果都已經喚醒了,直接就返回了
- 將已通知 goroutine 的數量加1
- 從等待喚醒的 goroutine 佇列中,獲取 head 指標指向的 goroutine,將其重新加入排程
- 被阻塞的 goroutine 可以繼續執行
cond.Broadcast 是如何通知等待的 goroutine 的?
- 判斷是否有沒有被喚醒的 goroutine,如果都已經喚醒了,直接就返回了
- 將等待通知的 goroutine 數量和已經通知過的 goroutine 數量設定成相等
- 遍歷等待喚醒的 goroutine 佇列,將所有的等待的 goroutine 都重新加入排程
- 所有被阻塞的 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” 。