举例来学cond原语

最近看了鸟窝的《Go并发编程实战课》,写的挺有意思的,打算后边弄些例子再回顾下并发原语。

今天来看看cond原语。

cond 是用于等待或通知场景下的并发原语,条件不满足时,阻塞(wait)一组goroutine;条件满足后,唤醒单个(signal)或所有(broadcast)阻塞的goroutine.

比如10个运动员跑步,都准备好了,裁判才发令的例子:

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
29
30
31
// 初始化带锁的条件变量
c := sync.NewCond(&sync.Mutex{})
var ready int

// 起10个协程,随机等待后模拟运动员就位,并记录就位人数(加锁更新)
for i := 0; i < 10; i++ {
go func(i int) {
time.Sleep(time.Duration(rand.Int63n(5)) * time.Second)

// 加锁更改等待条件
c.L.Lock()
ready++
c.L.Unlock()

log.Printf("运动员#%d 已准备就绪\n", i)
// 广播唤醒所有的等待者
// 这里用signal也可以,因为等待者只有一个main goroutine
c.Broadcast()
}(i)
}

// 等待条件满足:10人都就位
c.L.Lock()
for ready != 10 {
c.Wait()
log.Println("裁判员被唤醒一次")
}
c.L.Unlock()

// 所有的运动员是否就绪
log.Println("所有运动员都准备就绪。比赛开始,3,2,1, ......")

输出差不多如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2021/08/09 21:26:04 运动员#0 已准备就绪
2021/08/09 21:26:04 裁判员被唤醒一次
2021/08/09 21:26:04 运动员#4 已准备就绪
2021/08/09 21:26:04 裁判员被唤醒一次
2021/08/09 21:26:05 运动员#5 已准备就绪
2021/08/09 21:26:05 裁判员被唤醒一次
2021/08/09 21:26:05 运动员#3 已准备就绪
2021/08/09 21:26:05 运动员#9 已准备就绪
2021/08/09 21:26:05 裁判员被唤醒一次
2021/08/09 21:26:10 运动员#7 已准备就绪
2021/08/09 21:26:10 裁判员被唤醒一次
2021/08/09 21:26:11 运动员#1 已准备就绪
2021/08/09 21:26:11 裁判员被唤醒一次
2021/08/09 21:26:12 运动员#6 已准备就绪
2021/08/09 21:26:12 裁判员被唤醒一次
2021/08/09 21:26:12 运动员#2 已准备就绪
2021/08/09 21:26:12 裁判员被唤醒一次
2021/08/09 21:26:13 运动员#8 已准备就绪
2021/08/09 21:26:13 裁判员被唤醒一次
2021/08/09 21:26:13 所有运动员都准备就绪。比赛开始,321, ......

可以看出cond在更改条件或者检查条件时需要加锁处理,避免并发下读写不一致问题。

里边wait等待条件满足时比较特殊,需要加锁并在for循环中等待,为什么呢?

根据源码

1
2
3
4
5
6
7
8
9
10
11
func (c *Cond) Wait() {
c.checker.check()
// 更新等待groutine的计数: wait
t := runtime_notifyListAdd(&c.notify)
// 释放锁,防止阻塞后别的goroutine拿不到锁
c.L.Unlock()
// 切走当前goroutine,等待唤起
runtime_notifyListWait(&c.notify, t)
// 唤起后持有锁
c.L.Lock()
}

wait时,将当前goroutine加到等待队列(notifyList)前释放了锁,避免锁持有导致别的goroutine死锁;

唤起后,又持有锁,再持有锁前,可能有别的goroutine持有过锁,比如多次signal或者broadcast,
这里没法确定,当前goroutine唤起后条件没有改变,所以需要在for循环中检测条件是否依然满足

即,官方文档说明的:

1
2
3
4
5
6
//  c.L.Lock()
// for !condition() {
// c.Wait()
// }
// ... make use of condition ...
// c.L.Unlock()

如果用channel来实现,也可以做类似的事情:

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
// unbuffered
ch := make(chan int)
for i := 0; i < 10; i++ {
go func(i int) {
ch <- 1 // i is ready
}(i)
}

for i := 0; i < 10; i++ {
<-ch
}
println("all is ready!")


// buffered
ch := make(chan int, 10)
for i := 0; i < 10; i++ {
go func(i int) {
ch <- i
}(i)
}

for len(ch) != 10 {
time.Sleep(time.Millisecond)
}
println("all is ready!")

cond的优势在于其同时支持signalbroadcastchannel同时只能实现一种(closebroadcast,但只能用一次,不可重复调用)

waitGroup也可以模拟等待条件满足,但是针对的是主goroutine对确定数量goroutine的等待,不像cond只关心条件是否满足,对等待goroutine数目没有要求。

最后,推荐一个《concurrency-in-go》中提到的代码例子,fig-livelock-hallway

展示了用cond模拟的狭路相逢谁也过不去的活锁问题。

关于鸟窝的《Go并发编程实战课》,我链接放到这里,感兴趣的同学可以去听听,质量很高!

如有疑问,请文末留言交流或邮件:newbvirgil@gmail.com 本文链接 : https://newbmiao.github.io/2021/08/09/one-example-to-learn-cond.html