Go语言中sync.Cond的用法

The usage of sync.Cond in Golang

Posted by alovn on June 23, 2021

如果有一个任务需要满足一定条件才可以执行。一般都会想到使用channel可以实现,但是channel的方式比较适用于一对一的方式,一对多的话并不是很合适,这时候就可以使用sync.Cond,它可以实现多个协程间的通知。

sync.Cond值有一个sync.Locker类型的命名为L的字段,L具体值通常为*sync.Mutex或者*sync.RWMutex。

1
2
3
4
5
6
7
8
9
10
11
12
13
type Cond struct {
    noCopy noCopy
    // L is held while observing or changing the condition
    L Locker
    
    notify  notifyList
    checker copyChecker
}

// NewCond returns a new Cond with Locker l.
func NewCond(l Locker) *Cond {
    return &Cond{L: l}
}

*sync.Cond类型有三个方法:Wait(), Signal()、Broadcast()。

每个Cond值维护着一个先进先出等待协程队列。

c.Wait()

1
2
3
4
5
6
7
func (c *Cond) Wait() {
    c.checker.check()
    t := runtime_notifyListAdd(&c.notify)
    c.L.Unlock()
    runtime_notifyListWait(&c.notify, t)
    c.L.Lock()
}

通过源码可以看到一个c.Wait()调用将:

  1. 首先将当前协程推入到c所维护的等待协程队列。
  2. 然后调用c.L.Unlock()对c.L解锁。
  3. 然后使当前协程进入阻塞状态。(另一个协程通过调用c.Signal()或c.Broadcast()唤醒而重新进入运行状态)。

一旦当前协程重新进入运行状态,c.L.Lock()将被调用以试图重新进行加锁,调用成功后c.Wait()退出。

c.Signal()

1
2
3
4
func (c *Cond) Signal() {
    c.checker.check()
    runtime_notifyListNotifyOne(&c.notify)
}

调用c.Signal()将唤醒并移除c所维护的等待协程队列的第一个协程。

c.Broadcast()

1
2
3
4
func (c *Cond) Broadcast() {
    c.checker.check()
    runtime_notifyListNotifyAll(&c.notify)
}

调用c.Broadcast()将唤醒冰移除c所维护的等待协程队列中的所有协程。

一般情况下,c.Signal()和c.Broadcast()调用通常用来通知某个条件的状态发生了变化。c.Wait()应该在一个检查某个条件是否已经满足的循环中调用。

注意:c.Wait()必须在c.L字段值处于加锁状态的时候调用;否则会panic。而c.Signal()和c.Broadcast()不必在c.L处于加锁状态时调用。

示例

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
32
33
34
35
36
37
38
39
runtime.GOMAXPROCS(1)
cond := sync.NewCond(&sync.Mutex{})
for i := 0; i < 10; i++ {
    go func(x int) { //worker
        cond.L.Lock()
        defer cond.L.Unlock()
        cond.Wait()
        fmt.Println(x)
    }(i)
}
time.Sleep(time.Second)
fmt.Println("call Signal")
cond.Signal() //发送一个通知给已经获取锁的goroutine
time.Sleep(time.Second)
fmt.Println("call Signal")
cond.Signal()
time.Sleep(time.Second)
fmt.Println("call Signal")
cond.Signal()
time.Sleep(time.Second)
fmt.Println("call Broadcast")
cond.Broadcast() //广播给所有等待的goroutine
time.Sleep(time.Second)

// Output:
// call Signal
// 9
// call Signal
// 0
// call Signal
// 1
// call Broadcast
// 8
// 2
// 3
// 4
// 5
// 6
// 7