举例来学cond原语

共 4159字,需浏览 9分钟

 ·

2021-09-07 12:35

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 所有运动员都准备就绪。比赛开始,321, ......

可以看出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 int10)
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[1]

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

参考资料

[1]

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



推荐阅读


福利

我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复 ebook 获取;还可以回复「进群」,和数万 Gopher 交流学习。

浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报