Cond并发原语学习

Java 面试中,经常被问到的一个知识点就是等待 / 通知(wait/notify)机制。面试官经常 会这样考察候选人:请实现一个限定容量的队列(queue),当队列满或者空的时候,利 用等待 / 通知机制实现阻塞或者唤醒。

Go 标准库提供 Cond 原语目的是,为等待 / 通知场景下的并发问题提供支持。Cond 通 常应用于等待某个条件的一组 goroutine,等条件变为 true 的时候,其中一个 goroutine 或者所有的 goroutine 都会被唤醒执行。

从开发实践上,我们真正使用 Cond 的场景比较少,因为一旦遇到需要使用 Cond 的场 景,我们更多地会使用 Channel 的方式 ,因为那才是更地道的 Go 语言的写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
    c := sync.NewCond(&sync.Mutex{})
    var ready int
	for i := 0; i < 10; i++ {
        go func(i int) {
        time.Sleep(time.Duration(rand.Int63n(10)) * time.Second)
        // 加锁更改等待条件
        c.L.Lock()
        ready++
        c.L.Unlock()
        log.Printf("运动员#%d 已准备就绪\n", i)
        // 广播唤醒所有的等待者
        c.Broadcast()
        }(i)
	}
	c.L.Lock()
    for ready != 10 {
        c.Wait()
        log.Println("裁判员被唤醒一次")
    }
    c.L.Unlock()
	//所有的运动员是否就绪
	log.Println("所有运动员都准备就绪。比赛开始,3,2,1, ......")
}

Cond 的实现原理 其实,Cond 的实现非常简单,或者说复杂的逻辑已经被 Locker 或者 runtime 的等待队 列实现了。我们直接看看 Cond 的源码吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type Cond struct {
    noCopy noCopy
    // 当观察或者修改等待条件的时候需要加锁
    L Locker
    // 等待队列
    notify notifyList
    checker copyChecker
}
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
}

第一,同样的场景我们会使用其他的并发原语来替代。Go 特有的 Channel 类型,有一个 应用很广泛的模式就是通知机制,这个模式使用起来也特别简单。所以很多情况下,我们 会使用 Channel 而不是 Cond 实现 wait/notify 机制。 第二,对于简单的 wait/notify 场景,比如等待一组 goroutine 完成之后继续执行余下的 代码,我们会使用 WaitGroup 来实现。因为 WaitGroup 的使用方法更简单,而且不容易 出错。比如,上面百米赛跑的问题,就可以很方便地使用 WaitGroup 来实现。