举例来学cond原语
cond
是用于等待或通知场景下的并发原语,条件不满足时,阻塞(wait
)一组goroutine
;条件满足后,唤醒单个(signal
)或所有(broadcast
)阻塞的goroutine
.
比如 10 个运动员跑步,都准备好了,裁判才发令的例子:
// 初始化带锁的条件变量
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, ......")
输出差不多如下
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
循环中等待,为什么呢?
根据源码
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
循环中检测条件是否依然满足
即,官方文档说明的:
// c.L.Lock()
// for !condition() {
// c.Wait()
// }
// ... make use of condition ...
// c.L.Unlock()
如果用channel
来实现,也可以做类似的事情:
// 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
的优势在于其同时支持signal
和broadcast
,channel
同时只能实现一种(close
算broadcast
,但只能用一次,不可重复调用)
用waitGroup
也可以模拟等待条件满足,但是针对的是主goroutine
对确定数量goroutine
的等待,不像cond
只关心条件是否满足,对等待goroutine
数目没有要求。
最后,推荐一个《concurrency-in-go》中提到的代码例子,fig-livelock-hallway[1]
展示了用cond
模拟的狭路相逢谁也过不去的活锁问题。
参考资料
fig-livelock-hallway: https://github.com/kat-co/concurrency-in-go-src/blob/master/an-introduction-to-concurrency/why-is-concurrency-hard/deadlocks-livelocks-and-starvation/livelock/fig-livelock-hallway.go
推荐阅读