這一次,徹底搞懂 Go Cond
本文會(huì)從源碼角度去深入剖析下 sync.Cond。Go 日常開發(fā)中 sync.Cond 可能是我們用的較少的控制并發(fā)的手段,因?yàn)榇蟛糠謭鼍跋露急?Channel 代替了。還有就是 sync.Cond 使用確實(shí)也蠻復(fù)雜的。
比如下面這段代碼:
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 來實(shí)現(xiàn)
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 實(shí)現(xiàn)這個(gè)方法還是不容易的。我想這應(yīng)該是 sync.Cond 唯一有用武之地的地方。
先列出來一些問題吧,可以帶著這些問題來閱讀本文:
cond.Wait本身就是阻塞狀態(tài),為什么 cond.Wait 需要在循環(huán)內(nèi) ? sync.Cond 如何觸發(fā)不能復(fù)制的 panic ? 為什么 sync.Cond 不能被復(fù)制 ? cond.Signal 是如何通知一個(gè)等待的 goroutine ? cond.Broadcast 是如何通知等待的 goroutine 的?
源碼剖析




sync.Cond 排隊(duì)動(dòng)圖
cond.Wait 是阻塞的嗎?是如何阻塞的?
是阻塞的。不過不是 sleep 這樣阻塞的。
調(diào)用 goparkunlock 解除當(dāng)前 goroutine 的 m 的綁定關(guān)系,將當(dāng)前 goroutine 狀態(tài)機(jī)切換為等待狀態(tài)。等待后續(xù) goready 函數(shù)時(shí)候能夠恢復(fù)現(xiàn)場。
cond.Signal 是如何通知一個(gè)等待的 goroutine ?
判斷是否有沒有被喚醒的 goroutine,如果都已經(jīng)喚醒了,直接就返回了 將已通知 goroutine 的數(shù)量加1 從等待喚醒的 goroutine 隊(duì)列中,獲取 head 指針指向的 goroutine,將其重新加入調(diào)度 被阻塞的 goroutine 可以繼續(xù)執(zhí)行
cond.Broadcast 是如何通知等待的 goroutine 的?
判斷是否有沒有被喚醒的 goroutine,如果都已經(jīng)喚醒了,直接就返回了 將等待通知的 goroutine 數(shù)量和已經(jīng)通知過的 goroutine 數(shù)量設(shè)置成相等 遍歷等待喚醒的 goroutine 隊(duì)列,將所有的等待的 goroutine 都重新加入調(diào)度 所有被阻塞的 goroutine 可以繼續(xù)執(zhí)行
cond.Wait本身就是阻塞狀態(tài),為什么 cond.Wait 需要在循環(huán)內(nèi) ?
我們能注意到,調(diào)用 cond.Wait 的位置,使用的是 for 的方式來調(diào)用 wait 函數(shù),而不是使用 if 語句。
這是由于 wait 函數(shù)被喚醒時(shí),存在虛假喚醒等情況,導(dǎo)致喚醒后發(fā)現(xiàn),條件依舊不成立。因此需要使用 for 語句來循環(huán)地進(jìn)行等待,直到條件成立為止。
使用中注意點(diǎn)
1. 不能不加鎖直接調(diào)用 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 內(nèi)部會(huì)先調(diào)用 c.L.Unlock(),來先釋放鎖。如果調(diào)用方不先加鎖的話,會(huì)觸發(fā)“fatal error: sync: unlock of unlocked mutex”。關(guān)于 mutex 的使用方法,推薦閱讀下《這可能是最容易理解的 Go Mutex 源碼剖析》
2. 為什么不能 sync.Cond 不能復(fù)制 ?
sync.Cond 不能被復(fù)制的原因,并不是因?yàn)?sync.Cond 內(nèi)部嵌套了 Locker。因?yàn)?NewCond 時(shí)傳入的 Mutex/RWMutex 指針,對(duì)于 Mutex 指針復(fù)制是沒有問題的。
主要原因是 sync.Cond 內(nèi)部是維護(hù)著一個(gè) notifyList。如果這個(gè)隊(duì)列被復(fù)制的話,那么就在并發(fā)場景下導(dǎo)致不同 goroutine 之間操作的 notifyList.wait、notifyList.notify 并不是同一個(gè),這會(huì)導(dǎo)致出現(xiàn)有些 goroutine 會(huì)一直堵塞。
這里留下一個(gè)問題,sync.Cond 內(nèi)部是有一段代碼 check sync.Cond 是不能被復(fù)制的,下面這段代碼能觸發(fā)這個(gè) panic 嗎?
package main
import (
"fmt"
"sync"
)
func main() {
cond1 := sync.NewCond(new(sync.Mutex))
cond := *cond1
fmt.Println(cond)
}
有興趣的可以動(dòng)手嘗試下,以及嘗試下如何才能觸發(fā)這個(gè)panic "sync.Cond is copied” 。
sync.Cond 的剖析到這里基本就結(jié)束了。有什么想跟我交流的,歡迎評(píng)論區(qū)留言。
推薦閱讀
