Go微服务实战
上QQ阅读APP看书,第一时间看更新

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的用法。