![Go微服务实战](https://wfqqreader-1252317822.image.myqcloud.com/cover/523/36109523/b_36109523.jpg)
8.2.4 sync.Cond
Cond是条件变量,其作用是通过某个条件控制多个goroutine。如果满足条件,goroutine可以继续向下执行;如果不满足条件,goroutine则会进入等待状态。
Cond内部维护了一个notifyList,一旦goroutine不满足条件,就会进入notifyList并进入等待状态。即使后续条件满足,也需要其他的程序通过Broadcast()或者Signal()来唤醒notifyList内的goroutine。
下面来看一下Cond的结构体和方法:
type Cond struct{ noCopy noCopy //L用来在读写Cond时加锁 L Locker //以下是包外不可见变量 notify notifyList //通知列表 checker copyChecker } func NewCond(l Locker) *Cond func (c *Cond) Broadcast() func (c *Cond) Signal() func (c *Cond) Wait()
Broadcast方法用于向所有等待的goroutine发送通知—通知条件已经满足;Signal方法用于向特定的单个goroutine发送条件已满足的通知;如果条件不满足,Wait方法用于发送等待通知。
注意
在进行条件判断时,必须使用互斥锁保证条件判断的安全,也就是说判断的时候不会被其他goroutine修改条件。这也是Cond结构体中有一个Locker类型的L变量的原因。
一般情况下,sync.Cond的使用方式如下:
c.L.Lock() for !condition(){ c.Wait() } …对于condition的操作… c.L.Unlock()
注意这里使用的是for循环,而不是if,但不会导致Cond一直处于加锁状态,这与Wait方法的实现方式有关:
func (c *Cond) Wait() { c.checker.check() t := runtime_notifyListAdd(&c.notify)//获取notifyList c.L.Unlock() runtime_notifyListWait(&c.notify,t)//进入等待模式 c.L.Lock() }
可以看到,Wait方法内会自动解锁,并且进入等待模式,这时其他goroutine可以加锁,然后进行条件判断,如果不满足条件则也会进入等待模式。在进入等待模式以后,Wait方法不会执行最后一行的c.L.Lock(),除非方法外部使用Broadcast或Signal方法唤醒所有等待的goroutine,这时所有的goroutine会被唤醒,开始去执行最后一行的加锁动作,加锁以后要重新回到for循环去执行条件判断,如果满足条件就跳过循环继续往下执行,不满足条件又会进入等待状态。所以此处必须用for循环。
下面来看一个示例:
book/ch08/8.2/cond/cond.go
1. package main
2.
3. import (
4. "fmt"
5. "sync"
6. "time"
7. )
8.
9. var (
10. ready = false
11. singerNum = 3
12. )
13.
14. func Sing(singerId int,c *sync.Cond) {
15. fmt.Printf("Singer (%d) is ready\n",singerId)
16. c.L.Lock()
17. for !ready {
18. fmt.Printf("Singer (%d) is waiting\n",singerId)
19. c.Wait()
20. }
21. fmt.Printf("Singer (%d) sing a song\n",singerId)
22. ready = false
23. c.L.Unlock()
24. }
25.
26. func main() {
27. cond := sync.NewCond(&sync.Mutex{})
28. for i:=0;i<singerNum;i++{
29. go Sing(i,cond)
30. }
31. time.Sleep(3*time.Second)
32.
33. for i:=0;i<singerNum;i++{
34. ready = true
35. //cond.Broadcast() //自行试验用Broadcast替换Signal方法的效果
36. cond.Signal()
37. time.Sleep(3*time.Second)
38. }
39. }
这个示例模拟的是一个演唱会,歌手做好准备以后通过sync.Cond控制通知歌手开始唱歌,直到所有的歌手都唱完以后程序结束。显然,这里模拟的是每个歌手唱一首歌,且不存在合唱的情形。
第9行至第12行定义了两个变量:ready代表歌手准备好了,也就是goroutine准备好了;singerNum代表歌手的数量。
第14行至第24行的Sing方法中,singerId是歌手编号,c变量是sync.Cond条件变量。第16行开始加锁,然后判断是否满足条件,如果ready变量是false,则当前goroutine进入等待状态。如果当前goroutine被唤醒且ready变量变为true,则执行第21行和第22行。注意,在执行第21行和第22行的代码期间还是加锁的,所以第22行改变ready变量为false可以保证其他goroutine无法继续唱歌,需要等待Signal或Broadcast方法。
第27行定义条件变量,此处要注意新建条件变量的方式。
第28行至第30行,启动3个goroutine运行Sing方法,此时的ready变量是false,所以所有的歌手都是等待状态。
第31行的休眠是为了保证goroutine都启动了。
第33行至第38行,执行循环,每次都把ready变为true,然后调用Signal,每次休眠都是为了让Sing方法有足够的时间执行完。此处请读者思考,既然Signal方法是每次通知一个goroutine,那么第22行的ready=false代码有什么用呢?代码在第31行以前就把所有的goroutine放入等待状态了,而Signal每次只唤醒一个goroutine,那么设置为false又有什么意义呢?答案是没有意义,之所以加上第22行代码,是为了让读者测试第35行的Broadcast方法,只有加上了第22行代码,才需要执行Broadcast方法3次。
下面来看看使用Signal方法执行的结果:
Singer (2) is ready Singer (2) is waiting Singer (1) is ready Singer (1) is waiting Singer (0) is ready Singer (0) is waiting Singer (2) sing a song Singer (1) sing a song Singer (0) sing a song
改为Broadcast方法后程序执行的打印结果如下:
Singer (1) is ready Singer (1) is waiting Singer (0) is ready Singer (0) is waiting Singer (2) is ready Singer (2) is waiting Singer (0) sing a song Singer (1) is waiting Singer (2) is waiting Singer (1) sing a song Singer (2) is waiting Singer (2) sing a song
通过这个示例,读者可以比较全面地了解sync.Cond的用法。