Cond
Go 语言在标准库中提供的 Cond
其实是一个条件变量,通过 Cond
我们可以让一系列的 Goroutine 都在触发某个事件或者条件时才被唤醒,每一个 Cond
结构体都包含一个互斥锁 L
,我们先来看一下 Cond
是如何使用的:
总结
Cond
提供了类似队列FIFO的等待机制,同时也提供Signal唤醒队列最久一个
、Broadcast全部唤醒
- 相比于使用
for {}
忙碌等待,使用Cond
能够在遇到长时间条件无法满足时将当前处理器让出的功能,如果我们合理使用还是能够在一些情况下提升性能 Wait
方法在调用之前一定要使用L.Lock
持有该资源,否则会发生panic
导致程序崩溃;Signal
方法唤醒的 Goroutine 都是队列最前面、等待最久的 Goroutine;Broadcast
虽然是广播通知全部等待的 Goroutine,但是真正被唤醒时也是按照一定顺序的;
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"time"
)
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)
/*
* Notify函数让signal包将输入信号转发到c。如果没有列出要传递的信号,
会将所有输入信号传递到c;否则只传递列出的输入信号。
signal包不会为了向c发送信息而阻塞(就是说如果发送时c阻塞了,signal包会直接放弃):
调用者应该保证c有足够的缓存空间可以跟上期望的信号频率。对使用单一信号用于通知的通道,缓存为1就足够了。
signal.Notify(ch, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM,
syscall.SIGSTOP, syscall.SIGUSR1)
*/
signal.Notify(ch, os.Interrupt)
<-ch
}
func broadcast(c *sync.Cond) {
c.L.Lock()
c.Broadcast()
c.L.Unlock()
}
func listen(c *sync.Cond) {
c.L.Lock()
c.Wait()
fmt.Println("listen")
c.L.Unlock()
}
在上述代码中我们同时运行了 11 个 Goroutine,其中的 10 个 Goroutine 会通过 Wait
等待期望的信号或者事件,而剩下的一个 Goroutine 会调用 Broadcast
方法通知所有陷入等待的 Goroutine,当调用 Boardcast
方法之后,就会打印出 10 次 "listen"
并结束调用。
注意: 是调用了Broadcast之后,瞬间打印的
结构体
Cond
的结构体中包含 noCopy
和 copyChecker
两个字段,前者用于保证 Cond
不会再编译期间拷贝,后者保证在运行期间发生拷贝会直接 panic
,持有的另一个锁 L
其实是一个接口 Locker
,任意实现 Lock
和 Unlock
方法的结构体都可以作为 NewCond
方法的参数:
type Cond struct {
noCopy noCopy
L Locker
notify notifyList
checker copyChecker
}
结构体中最后的变量 notifyList
其实也就是为了实现 Cond
同步机制,该结构体其实就是一个 Goroutine
的链表:
type notifyList struct {
wait uint32
notify uint32
lock mutex
head *sudog
tail *sudog
}
在这个结构体中,head
和 tail
分别指向的就是整个链表的头和尾,而 wait
和 notify
分别表示当前正在等待的 Goroutine 和已经通知到的 Goroutine,我们通过这两个变量就能确认当前待通知和已通知的 Goroutine。
操作
Cond
对外暴露的 Wait
方法会将当前 Goroutine 陷入休眠状态,它会先调用 runtime_notifyListAdd
将等待计数器 +1
,然后解锁并调用 runtime_notifyListWait
等待其他 Goroutine 的唤醒:
func (c *Cond) Wait() {
c.checker.check()
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
func notifyListAdd(l *notifyList) uint32 {
return atomic.Xadd(&l.wait, 1) - 1
}
notifyListWait
方法的主要作用就是获取当前的 Goroutine 并将它追加到 notifyList
链表的最末端:
func notifyListWait(l *notifyList, t uint32) {
lock(&l.lock)
if less(t, l.notify) {
unlock(&l.lock)
return
}
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 追加到链表的末端之外,我们还会调用 goparkunlock
陷入休眠状态,该函数也是在 Go 语言切换 Goroutine 时经常会使用的方法,它会直接让出当前处理器的使用权并等待调度器的唤醒。
Cond
对外提供的 Signal
和 Broadcast
方法就是用来唤醒调用 Wait
陷入休眠的 Goroutine,从两个方法的名字来看,前者会唤醒队列最前面的 Goroutine,后者会唤醒队列中全部的 Goroutine:
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
notifyListNotifyAll
方法会从链表中取出全部的 Goroutine 并为它们依次调用 readyWithTime
,该方法会通过 goready
将目标的 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,但是这里唤醒的顺序其实也是按照加入队列的先后顺序,先加入的会先被 goready
唤醒,后加入的 Goroutine 可能就需要等待调度器的调度。
而 notifyListNotifyOne
函数就只会从 sudog
构成的链表中满足 sudog.ticket == l.notify
的 Goroutine 并通过 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.ticket == 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
}
}
}
在一般情况下我们都会选择在不满足特定条件时调用 Wait
陷入休眠,当某些 Goroutine 检测到当前满足了唤醒的条件,就可以选择使用 Signal
通知一个或者 Broadcast
通知全部的 Goroutine 当前条件已经满足,可以继续完成工作了。