Go语言sync.Cond使用方法详解
作者:码一行
概述
每一个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方法会唤醒队列最前面的Goroutinesync.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都是队列最前面、等待最久的Goroutinesync.Cond.Broadcast会按照一定顺序广播通知等待的全部Goroutine
到此这篇关于Go语言sync.Cond使用方法详解的文章就介绍到这了,更多相关Go语言sync.Cond内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
