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 的基本使用,最后列举了一个非常容易踩的「坑」。


参考资料:



我要分享资源 | 解压密码 | 交流群 | 12搬运工