01
介绍
在 Go 语言中,Cond 实现一个条件变量,协助解决等待或通知事件场景的并发执行问题,通常用于等待某个条件的一组 goroutine。这个条件需要一组 goroutine 共同协作完成,如果条件为 false,所有等待这个条件的 goroutine 将会被阻塞,当这个条件变为 true 时,所有等待这个条件的其中一个 goroutine 或者所有 goroutine 会被唤醒。
02
基本用法
通过阅读源码,我们可以发现 Cond 关联一个 Locker L(通常是 *Mutex 或 *RWMutex),在更新条件和调用 Wait 方法时必须持有该锁 L。并且,首次使用后不得复制 Cond。通常,使用 NewCond 函数创建一个 Cond。
// A Cond must not be copied after first use. type Cond struct { noCopy noCopy // L is held while observing or changing the condition L Locker notify notifyList checker copyChecker } // NewCond returns a new Cond with Locker l. func NewCond(l Locker) *Cond { return &Cond{L: l} } func (c *Cond) Wait() { c.checker.check() t := runtime_notifyListAdd(&c.notify) c.L.Unlock() runtime_notifyListWait(&c.notify, t) c.L.Lock() } func (c *Cond) Signal() { c.checker.check() runtime_notifyListNotifyOne(&c.notify) } func (c *Cond) Broadcast() { c.checker.check() runtime_notifyListNotifyAll(&c.notify) }
Cond 只有 3 个方法,分别是 Wait、Signal 和 Broadcast。下面分别介绍一下这 3 个方法。
- Wait:把调用者放入等待队列中并阻塞,直到被 Signal 方法或 Broadcast 方法在等待队列中移除并唤醒。调用 Wait 方法之前,调用者必须持有锁。
- Signal:调用者将等待队列中的 goroutine 移除第一个,并唤醒被移除的 goroutine。
- Broadcast:调用者将等待队列中的所有 goroutine 全部移除,并全部唤醒。
了解了 Cond 的 3 个方法,我们通过实现一个「学生报名参加课外活动」的简单示例,演示如何使用 Cond。
其中,需要注意的是 Wait 方法。调用者在调用 Wait 方法之前,必须持有锁,并且每次调用都要检查辅助条件变量 count。
03
实现原理
通过阅读 Part 02 的源码,可以发现 Cond 实现非常简单,主要是通过 Wait 方法将调用者放入一个等待队列中并阻塞,其他 goroutine 去检查或更新条件变量,然后通过调用 Signal 方法或 Broadcast 方法唤醒等待队列中的一个或全部 goroutine。
04
踩坑
使用 Cond,最容易踩的坑就是调用 Wait 方法之前,调用者没有持有锁或没有检查辅助条件。
在 Part 02 的示例代码中,假如把调用 Wait 方法前后的加锁和释放锁的代码注释掉,运行代码会导致程序 panic。原因是调用 Wait 方法,会先把调用者放入等待队列中,然后释放锁。此时如果在未持有锁时调用释放锁的方法,就会导致程序 panic。
还有就是等待队列中的 goroutine 被唤醒,并不代表条件满足,还要再次检查辅助条件是否满足。
05
总结
本文开篇介绍了 Cond 的用途,然后结合源码介绍了 Cond 的实现和 3 个方法,并通过一个「学生报名参加课外活动」的模拟示例演示了 Cond 的基本使用,最后列举了一个非常容易踩的「坑」。