最近看了鸟窝的《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
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) c.Broadcast() }(i) }
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 所有运动员都准备就绪。比赛开始,3,2,1, ......
|
可以看出cond
在更改条件或者检查条件时需要加锁处理,避免并发下读写不一致问题。
里边wait
等待条件满足时比较特殊,需要加锁并在for
循环中等待,为什么呢?
根据源码
1 2 3 4 5 6 7 8 9 10 11
| func (c *Cond) Wait() { c.checker.check() t := runtime_notifyListAdd(&c.notify) c.L.Unlock() runtime_notifyListWait(&c.notify, t) c.L.Lock() }
|
wait
时,将当前goroutine
加到等待队列(notifyList
)前释放了锁,避免锁持有导致别的goroutine
死锁;
唤起后,又持有锁,再持有锁前,可能有别的goroutine
持有过锁,比如多次signal
或者broadcast
,
这里没法确定,当前goroutine
唤起后条件没有改变,所以需要在for
循环中检测条件是否依然满足
即,官方文档说明的:
如果用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
| ch := make(chan int) for i := 0; i < 10; i++ { go func(i int) { ch <- 1 }(i) }
for i := 0; i < 10; i++ { <-ch } println("all is ready!")
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
的优势在于其同时支持signal
和broadcast
,channel
同时只能实现一种(close
算broadcast
,但只能用一次,不可重复调用)
用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