最近在学习Go语言高性能编程时看到了sync.Cond条件变量这个概念,一时有些难以理解。查阅资料后对其有了些认知,特此记录。

概念

sync包提供了条件变量类型sync.Cond,它可以和互斥锁或读写锁组合使用,用来协调想要访问共享变量的协程。其主要作用机制是在对应的共享资源状态发生变化时,通知其它因此而阻塞的协程。

定义

sync.Cond是一个结构体,其定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Each Cond has an associated Locker L (often a *Mutex or *RWMutex),
// which must be held when changing the condition and
// when calling the Wait method.
//
// A Cond must not be copied after first use.
type Cond struct {
noCopy noCopy

// L is held while observing or changing the condition
L Locker

notify notifyList
checker copyChecker
}

该类型可通过sync包中的func NewCond(l Locker) *Cond方法创建。

它有如下三个方法:

  • func (c *Cond) Broadcast()

    唤醒所有等待 c 的协程。

  • func (c *Cond) Signal()

    唤醒单个等待 c 的协程(如果有的话)。

  • func (c *Cond) Wait()

    自动解锁 c.L并暂停该协程执行,直到被唤醒。该函数在返回前会重新对c.L加锁。

应用场景

由以上描述可知,该结构的主要用法应该是多个协程调用Wait等待某个协程运行,该协程任务完毕后调用BroadcastSignal将等待的协程唤醒。即:多用在多个协程等待,一个协程通知的场景

举个生活中的小例子:

多位同学到达食堂,此时食堂还未做完饭,同学们需要等待食堂出餐后才能恰饭。

Go条件变量对以上场景进行描述,可写出如下代码:

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
40
41
42
43
package main

import (
"fmt"
"sync"
"time"
)

var (
ok = false // 是否做完饭
food = "" // 饭的名字
rwMutex = &sync.RWMutex{} // 读写锁,用于锁定饭名的修改
cond = sync.NewCond(rwMutex.RLocker()) // 条件变量使用读写锁中的读锁
)

func makeFood() {
// 做饭使用写锁(当然因为只有一个做饭协程,该锁并无实际意义)
rwMutex.Lock()
defer rwMutex.Unlock()
fmt.Println("食堂开始做饭!")
time.Sleep(3 * time.Second)
ok = true
food = "鱼香肉丝"
fmt.Println("食堂做完饭了!")
cond.Broadcast()
}

func waitToEat() {
cond.L.Lock()
defer cond.L.Unlock()
for !ok {
cond.Wait()
}
fmt.Println("总算吃到饭了,这顿吃的是", food)
}

func main() {
for i := 0; i < 3; i++ {
go waitToEat()
}
go makeFood()
time.Sleep(4 * time.Second)
}

运行该程序,结果如下:

1
2
3
4
5
食堂开始做饭!
食堂做完饭了!
总算吃到饭了,这顿吃的是 鱼香肉丝
总算吃到饭了,这顿吃的是 鱼香肉丝
总算吃到饭了,这顿吃的是 鱼香肉丝

与其它概念的区别

channel

众所周知,channel同样是 Go 中用于协程同步的概念,但与sync.Cond不同,channel多用于一对一通信,如果继承上面的例子,这次的场景应该是:

一位同学到达食堂,此时食堂还未做完饭,该同学需要等待食堂出餐后才能恰饭。

channel描述为:

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
package main

import (
"fmt"
"time"
)

var (
food = ""
ch = make(chan struct{})
)

func makeFood() {
fmt.Println("食堂开始做饭!")
time.Sleep(3 * time.Second)
food = "鱼香肉丝"
fmt.Println("食堂做完饭了!")
ch <- struct{}{}
}

func waitToEat() {
<-ch
fmt.Println("总算吃到饭了,这顿吃的是", food)
}

func main() {
go waitToEat()
go makeFood()
time.Sleep(4 * time.Second)
}

运行结果为:

1
2
3
食堂开始做饭!
食堂做完饭了!
总算吃到饭了,这顿吃的是 鱼香肉丝

sync.WaitGroup

sync.WaitGroup同样用于协程同步,但应用场景与sync.Cond刚好相反,后者多用于多协程等待,单协程通知,而前者多用于单协程等待多协程执行完毕

仍然使用上述例子:

一位同学到达食堂,此时食堂多个窗口均未做完饭,该同学想要等待每个窗口都做完饭后各点一份吃。(多少有点离谱了XD)

代码描述如下:

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
package main

import (
"fmt"
"sync"
"time"
)

var (
wg sync.WaitGroup
)

func makeFood() {
fmt.Println("窗口开始做饭!")
time.Sleep(3 * time.Second)
fmt.Println("窗口做完饭了!")
wg.Done()
}

func waitToEat() {
wg.Wait()
fmt.Println("oho,每个窗口来一份!")
}

func main() {
for i := 0; i < 3; i++ {
wg.Add(1)
go makeFood()
}
go waitToEat()
time.Sleep(4 * time.Second)
}

运行结果:

1
2
3
4
5
6
7
窗口开始做饭!
窗口开始做饭!
窗口开始做饭!
窗口做完饭了!
窗口做完饭了!
窗口做完饭了!
oho,每个窗口来一份!

结语

以上为个人的浅薄理解,如有错误欢迎在评论区指出,感谢!