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
方法会唤醒队列最前面的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
到此这篇关于Go语言sync.Cond使用方法详解的文章就介绍到这了,更多相关Go语言sync.Cond内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!