Go 同步原語 — sync-Con
概述
Go 語言標準庫中還包含條件變量 sync.Cond
,它可以讓一組 Goroutine
都在滿足特定條件時被喚醒。每一個sync.Cond
結構體在初始化時都需要傳入一個互斥鎖,我們可以通過下面的例子瞭解它的使用方法:
var status int64
func main(){
c := sync.NewCond(&sync.mutex{})
for i := 0; i < 10; i++ {
go listen(c)
}
time.Sleep(1 * time.Second)
go broadcast(c)
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
<-ch
}
func broadcast(c *sync.Cond) {
c.L.Lock()
atomic.StoreInt64(&status, 1)
c.Broadcast()
c.L.Unlock()
}
func listen(c *sync.Cond) {
c.L.Lock()
for atomic.LoadInt64(&status) != 1 {
c.Wait()
}
fmt.Println("listen")
c.L.Unlock()
}
運行結果:
listen
...
listen
上述代碼同時運行了 11 個Goroutine
,它們分別做了不同事情:
-
10 個
Goroutine
通過sync.Cond.Wait
等待特定條件滿足 -
1 個
Goroutine
會調用sync.Cond.Broadcast
喚醒所有陷入等待的Goroutine
調用sync.Cond.Broadcast
方法後,上述代碼會打印出 10 次 "listen" 並結束調用。
結構體
sync.Cond
的結構體中包含以下 4 個字段:
type Cond struct {
noCopy noCopy
L Locker
notify notifyList
checker copyChecker
}
-
noCopy —— 用於保證結構體不會在編譯期間複製
-
L —— 用於保護內部的
notify
字段,Locker
接口類型的變量 -
notify —— 一個
Goroutine
的鏈表,它是實現同步機制的核心結構 -
copyChecker —— 用於禁止運行期間發生的複製
type notifyList struct {
wait uint32
notify uint32
lock mutex
head *sudog
tail *sudog
}
在sync.notifyList
結構體中,head
和tail
分別指向鏈表的頭和尾,wait
和notify
分別表示當前正在等待的和已經通知的Goroutine
的索引。
接口
sync.Cond
對外暴露的sync.Cond.Wait
方法會令當前Goroutine
陷入休眠狀態,它的執行過程分成以下兩個步驟:
-
調用
runtime.notifyListAdd
將等待計時器加一併解鎖 -
調用
runtime.notifyListWait
等待其他Goroutine
被喚醒並對其加鎖
func (c *Cond) Wait () {
c.checker.check()
t := runtime_notifyListAdd(&c.notify) // runtime.notifyListAdd 的鏈接名
c.L.Unlock()
runtime_notifyListWait(&c.notify, t) //runtime.notifyListWait 的鏈接名
c.L.Lock()
}
func notifyListAdd(l *notifyList) uint32 {
return atomic.Xadd(&l.wait, 1) - 1
}
runtime.notifyListWait
會獲取當前Goroutine
並將它追加到Goroutine
通知鏈表的末端:
func notifyListWait(l *notifyList, t uint32) {
s := acquireSudog()
s.g = getg()
s.ticket = t
if l.tail == nil {
l.head = s
} else {
l.tail.next = s
}
l.tail = s
goparkunlock(&l.lock, waitReasonSyncCondWait, traceEvGoBlockCond, 3)
releaseSudog(s)
}
除了將當前Goroutine
追加到鏈表末端外,我們還會調用runtime.goparkunlock
令當前Goroutine
陷入休眠。該函數也是在 Go 語言切換Goroutine
時常用的方法,它會直接讓出當前處理器的使用權並等待調度器喚醒。
sync.Cond.Signal
和sync.Cond.Broadcast
方法就是用來喚醒陷入休眠的Goroutine
的,它們的實現有一些細微差別:
-
sync.Cond.Signal
方法會喚醒隊列最前面的Goroutine
-
sync.Cond.Broadcast
方法會喚醒隊列中全部Goroutine
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
runtime.notifyListNotifyOne
只會從sync.notifyList
鏈表中找到滿足sudog.ticket == l.notify
條件的Goroutine
,並通過runtime.readyWithTime
將其喚醒:
func notifyListNotifyOne(l *notifyList) {
t := l.notify
atomic.Store(&l.notify, t + 1)
for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
if s.tiket == t {
n := s.next
if p != nil {
p.next = n
} else {
l.head = n
}
if n == nil {
l.tail = p
}
s.next = nil
readyWithTime(s, 4)
return
}
}
}
runtime.notifyListNotifyAll
會依次通過runtime.readyWithTime
喚醒鏈表中的Goroutine
:
func notifyListNotifyAll(l *notifyList) {
s := l.head
l.head = nil
l.tail = nil
atomic.Store(&l.notify, atomic.Load(&l.wait))
for s != nil {
next := s.next
s.next = nil
readyWithTime(s, 4)
s = next
}
}
Goroutine
的喚醒順序也是按照加入隊列的先後順序,先加入的會先被喚醒,而後加入的Goroutine
可能需要等待調度器的調度。
一般情況下,我們會先調用sync.Cond.Wait
陷入休眠等待滿足期望條件,當滿足期望條件時,就可以選用sync.Cond.Signal
或者sync.Cond.Broadcast
喚醒一個或者全部Goroutine
。
小結
sync.Cond
不是常用的同步機制,但是在條件長時間無法滿足時,與使用for {}
進行忙碌等待相比,sync.Cond
能夠讓出處理器的使用權,提高CPU
的利用率。
使用時需要注意以下問題:
-
sync.Cond.Wait
在調用之前一定要先獲取互斥鎖,否則會觸發程序崩潰 -
sync.Cond.Signal
喚醒的Goroutine
都是隊列最前面、等待最久的Goroutine
-
sync.Cond.Broadcast
會按照一定順序廣播通知等待的全部Goroutine
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/CV9CNMDHZzYAga898BhrdA