概述
cond是go語言sync提供的條件變量,通過cond可以讓一系列的goroutine在觸發某個條件時才被喚醒。每一個cond結構體都包含一個鎖L。cond提供了三個方法:
- Signal:調用Signal之後可以喚醒單個goroutine。
- Broadcast:喚醒等待隊列中所有的goroutine。
- Wait:會把當前goroutine放入到隊列中等待獲取通知,調用此方法必須先Lock,不然方法裏會調用Unlock()報錯。
簡單使用
創建40個goroutine都wait阻塞住。調用Signal則喚醒第一個goroutine。調用Broadcast則喚醒所有等待的goroutine。
package main
import (
"fmt"
"sync"
"time"
)
var locker = new(sync.Mutex)
var cond = sync.NewCond(locker)
func test(x int) {
cond.L.Lock() //獲取鎖
cond.Wait() //等待通知 暫時阻塞
fmt.Println(x)
time.Sleep(time.Second * 1)
cond.L.Unlock() //釋放鎖
}
func main() {
for i := 0; i < 40; i++ {
go test(i)
}
fmt.Println("start all")
time.Sleep(time.Second * 3)
fmt.Println("broadcast")
cond.Signal() // 下發一個通知給已經獲取鎖的goroutine
time.Sleep(time.Second * 3)
cond.Signal() // 3秒之後 下發一個通知給已經獲取鎖的goroutine
time.Sleep(time.Second * 3)
cond.Broadcast() //3秒之後 下發廣播給所有等待的goroutine
time.Sleep(time.Second * 60)
}
源碼分析
Cond
type Cond struct {
noCopy noCopy
// 鎖的具體實現,通常爲 mutex 或者rwmutex
L Locker
// notifyList對象,維護等待喚醒的goroutine隊列,使用鏈表實現
notify notifyList
checker copyChecker
}
// 新建cond初始化cond對象
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
type notifyList struct {
// 等待數量
wait uint32
// 通知數量
notify uint32
// 鎖對象
lock mutex
// 鏈表頭
head *sudog
// 鏈表尾
tail *sudog
}
Wait
// 等待函數
func (c *Cond) Wait() {
c.checker.check()
// 等待計數器加1 看下面具體實現
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
//
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
// 此函數在sema.go中控制計數器加1
func notifyListAdd(l *notifyList) uint32 {
// This may be called concurrently, for example, when called from
// sync.Cond.Wait while holding a RWMutex in read mode.
return atomic.Xadd(&l.wait, 1) - 1
}
// 此函數在sema.go中
// 獲取當前goroutine 添加到鏈表末端,然後goparkunlock函數休眠阻塞當前goroutine
// goparkunlock函數會讓出當前處理器的使用權並等待調度器的喚醒
func notifyListWait(l *notifyList, t uint32) {
lock(&l.lock)
// Return right away if this ticket has already been notified.
if less(t, l.notify) {
unlock(&l.lock)
return
}
// Enqueue itself.
s := acquireSudog()
s.g = getg()
s.ticket = t
s.releasetime = 0
t0 := int64(0)
if blockprofilerate > 0 {
t0 = cputicks()
s.releasetime = -1
}
if l.tail == nil {
l.head = s
} else {
l.tail.next = s
}
l.tail = s
goparkunlock(&l.lock, "semacquire", traceEvGoBlockCond, 3)
if t0 != 0 {
blockevent(s.releasetime-t0, 2)
}
releaseSudog(s)
}
Broadcast
喚醒鏈表中所有的阻塞中的goroutine,還是使用readyWithTime來實現這個功能
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
// 源代碼在sema.go中
func notifyListNotifyAll(l *notifyList) {
// Fast-path: if there are no new waiters since the last notification
// we don't need to acquire the lock.
if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
return
}
// Pull the list out into a local variable, waiters will be readied
// outside the lock.
lock(&l.lock)
s := l.head
l.head = nil
l.tail = nil
// Update the next ticket to be notified. We can set it to the current
// value of wait because any previous waiters are already in the list
// or will notice that they have already been notified when trying to
// add themselves to the list.
atomic.Store(&l.notify, atomic.Load(&l.wait))
unlock(&l.lock)
// Go through the local list and ready all waiters.
for s != nil {
next := s.next
s.next = nil
readyWithTime(s, 4)
s = next
}
}
Signal
// 調用runtime_notifyListNotifyOne方法喚醒鏈表頭的goroutine
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
// runtime_notifyListNotifyOne具體實現 獲取鏈表頭部的G,然後調用readyWithTime喚醒goroutine
// 源代碼在sema.go中
func notifyListNotifyOne(l *notifyList) {
if atomic.Load(&l.wait) == atomic.Load(&l.notify) {
return
}
lock(&l.lock)
t := l.notify
if t == atomic.Load(&l.wait) {
unlock(&l.lock)
return
}
atomic.Store(&l.notify, t+1)
for p, s := (*sudog)(nil), l.head; s != nil; p, s = s, s.next {
if s.ticket == t {
n := s.next
if p != nil {
p.next = n
} else {
l.head = n
}
if n == nil {
l.tail = p
}
unlock(&l.lock)
s.next = nil
readyWithTime(s, 4)
return
}
}
unlock(&l.lock)
}