Go语言中的Cond是一个条件变量,用于在多个goroutine之间同步地等待和通知。它可以用于实现线程间的互斥和同步。Cond通常与Mutex一起使用,以便在共享数据结构上进行访问控制。
sync.Cond 的定义
// 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
}
每个 Cond 实例都会关联一个锁 L(互斥锁 Mutex,或读写锁 RWMutex),当修改条件或者调用 Wait 方法时,必须加锁。
在使用Cond时,通常需要与Mutex配合使用。
- 使用Mutex来保护共享数据结构;
- 在Cond等待前获取锁,等待条件变量;
- 在满足条件后使用Cond.Signal()或Cond.Broadcast()来唤醒等待的goroutine;释放锁。
- 这样可以确保在修改共享数据结构时不会有多个goroutine同时修改导致竞争条件。
Cond方法
- NewCond 创建实例,NewCond 创建 Cond 实例时,需要关联一个锁。
- Signal():S 只唤醒任意 1 个等待条件变量 c 的 goroutine,无需锁保护。
- Broadcast():Broadcast 唤醒所有等待条件变量 c 的 goroutine,无需锁保护。
- Wait():阻塞当前 goroutine 直到接收到通知,调用 Wait 会自动释放锁 c.L。
func NewCond(l Locker) *Cond
func (c *Cond) Broadcast()
func (c *Cond) Signal()
func (c *Cond) Wait()
sync.Cond 的使用场景
sync.Cond 条件变量用来协调想要访问共享资源的那些 goroutine,当共享资源的状态发生变化的时候,它可以用来通知被互斥锁阻塞的 goroutine。
sync.Cond 基于互斥锁/读写锁,它和互斥锁的区别是什么呢?
互斥锁 sync.Mutex 通常用来保护临界区和共享资源,条件变量 sync.Cond 用来协调想要访问共享资源的 goroutine。
sync.Cond 经常用在多个 goroutine 等待,一个 goroutine 通知(事件发生)的场景。
如果是一个通知,一个等待,使用互斥锁或 channel 就能搞定了。
使用示例
示例1
package main
import (
"fmt"
"sync"
"time"
)
// 互斥锁需要保护的条件变量
var done = false
// 调用 Wait() 等待通知,直到 done 为 true。
func read(name string, c *sync.Cond) {
c.L.Lock()
for !done {
// 调用wait
c.Wait()
}
fmt.Println(name, "--------------- starts reading")
c.L.Unlock()
}
func write(name string, c *sync.Cond) {
fmt.Println(name, "starts writing")
// 暂停1s
time.Sleep(time.Second)
c.L.Lock()
// 将 done 置为 true
done = true
c.L.Unlock()
fmt.Println(name, "wakes all")
// Broadcast
c.Broadcast()
}
func main() {
cond := sync.NewCond(&sync.Mutex{})
go read("reader1", cond)
go read("reader2", cond)
go read("reader3", cond)
write("writer", cond)
time.Sleep(time.Second * 3)
}
输出结果:
writer starts writing
writer wakes all
reader1 --------------- starts reading
reader3 --------------- starts reading
reader2 --------------- starts reading
- done 即互斥锁需要保护的条件变量。
- read() 调用 Wait() 等待通知,直到 done 为 true。
- write() 接收数据,接收完成后,将 done 置为 true,调用 Broadcast() 通知所有等待的协程。
- write() 中的暂停了 1s,一方面是模拟耗时,另一方面是确保前面的 3 个 read 协程都执行到 Wait(),处于等待状态。main 函数最后暂停了 3s,确保所有操作执行完毕。
package main
import (
"fmt"
"log"
"math/rand"
"sync"
"time"
)
var ready = 0
var cond = sync.NewCond(&sync.Mutex{})
var count = 0
func main() {
for i := 0; i < 10; i++ {
go readyGo(i)
}
wake()
log.Println("所有运动员都准备就绪,比赛开始。。。")
}
func readyGo(i int) {
time.Sleep(time.Second * time.Duration(rand.Int63n(10)))
// 加锁更改等待条件
cond.L.Lock()
ready++
cond.L.Unlock()
fmt.Printf("======运动员%d已准备就绪 \n", i)
// 广播唤醒等待者,这里可以使用Broadcast和Signal
cond.Signal()
}
func wake() {
cond.L.Lock()
for ready != 10 {
cond.Wait()
count++
fmt.Printf("裁判员被唤醒%d次 \n", count)
}
cond.L.Unlock()
}
示例2
10 个运动员进入赛场之后需要先做拉伸活动活动筋骨 ,在自己的赛道上做好准备;等所有的运动员都准备好之后,裁判员才会打响发令枪。
每个运动员做好准备之后,将 ready 加一,表明自己做好准备了,同时调用 Broadcast 方法通知裁判员。因为裁判员只有一个,所以这里可以直接替换成 Signal 方法调用。
调用 Broadcast 方法的时候,我们并没有请求 c.L 锁,只是在更改等待变量的时候才使用到了锁。
裁判员会等待运动员都准备好。虽然每个运动员准备好之后都唤醒了裁判员,但是裁判员被唤醒之后需要检查等待条件是否满足(运动员都准备好了)。可以看到,裁判员被唤醒之后一定要检查等待条件,如果条件不满足还是要继续等待。
======运动员7已准备就绪
裁判员被唤醒1次
======运动员0已准备就绪
裁判员被唤醒2次
======运动员5已准备就绪
裁判员被唤醒3次
======运动员4已准备就绪
裁判员被唤醒4次
======运动员9已准备就绪
裁判员被唤醒5次
======运动员8已准备就绪
裁判员被唤醒6次
======运动员6已准备就绪
裁判员被唤醒7次
======运动员3已准备就绪
裁判员被唤醒8次
======运动员1已准备就绪
裁判员被唤醒9次
======运动员2已准备就绪
裁判员被唤醒10次
所有运动员都准备就绪,比赛开始。。。
Cond实现原理
type Cond struct {
noCopy noCopy
// 当观察或者修改等待条件的时候需要加锁
L Locker
// 等待队列
notify notifyList
checker copyChecker
}
func NewCond(l Locker) *Cond {
return &Cond{L: l}
}
func (c *Cond) Wait() {
c.checker.check()
// 增加到等待队列中
t := runtime_notifyListAdd(&c.notify)
c.L.Unlock()
// 阻塞休眠直到被唤醒
runtime_notifyListWait(&c.notify, t)
c.L.Lock()
}
func (c *Cond) Signal() {
c.checker.check()
runtime_notifyListNotifyOne(&c.notify)
}
func (c *Cond) Broadcast() {
c.checker.check()
runtime_notifyListNotifyAll(&c.notify)
}
- runtime_notifyListXXX 是运行时实现的方法,实现了一个等待 / 通知的队列。
- copyChecker 是一个辅助结构,可以在运行时检查 Cond 是否被复制使用。
- Signal 和 Broadcast 只涉及到 notifyList 数据结构,不涉及到锁。
Wait 把调用者加入到等待队列时会释放锁,在被唤醒之后还会请求锁。在阻塞休眠期间,调用者是不持有锁的,这样能让其他 goroutine 有机会检查或者更新等待变量。
参考
- https://juejin.cn/post/7194704072136966181
- https://www.cnblogs.com/huiyichanmian/p/14463844.html