解锁 Go 的 sync.Cond:晚餐铃模式
发布: (2025年12月30日 GMT+8 02:51)
4 min read
原文: Dev.to
Source: Dev.to
晚餐铃的类比
想象有一位父亲(发布者)在为他十个饥饿的孩子(订阅者)做煎饼。
-
孩子们每 5 秒跑进厨房一次,检查盘子,发现是空的,然后离开。
- CPU:高(孩子们来回跑)。
- 争用:厨房门(互斥锁)不断被锁定和解锁。
-
父亲做好一块煎饼后,需要逐个走到每个孩子面前把食物递过去。
- 延迟:第 10 个孩子得到食物的时间远晚于第 1 个。
- 耦合:父亲忙于递送而不是继续烹饪。
现在,让孩子们坐在桌旁并入睡。父亲敲响一只响亮的铃(广播)。
- 结果:所有人瞬间醒来。
- 效率:等待期间 CPU 使用为零。
在 Go 中,sync.Cond(条件变量)扮演的就是这只晚餐铃的角色。它总是与 sync.Mutex(厨房钥匙)配合使用。没有钥匙,你无法检查食物(数据)是否可用。
模式
package main
import (
"fmt"
"sync"
"time"
)
type Kitchen struct {
mu sync.Mutex
cond *sync.Cond
pancakes int
}
func NewKitchen() *Kitchen {
k := &Kitchen{}
// 将 Cond 与锁关联
k.cond = sync.NewCond(&k.mu)
return k
}
// 厨师创建数据并敲响铃
func (k *Kitchen) Cook() {
k.mu.Lock() // 1. 拿钥匙
k.pancakes++ // 2. 做食物
fmt.Println("Pancake ready!")
k.mu.Unlock() // 3. 放回钥匙
// 4. 敲响铃!
// 在不持有锁的情况下广播是安全的,
// 但这样做通常更清晰。
k.cond.Broadcast()
}
// 每个孩子尝试吃一块煎饼
func (k *Kitchen) Eat(id int) {
k.mu.Lock() // 1. 拿钥匙进入厨房
defer k.mu.Unlock()
// 2. 检查循环
// 为什么要用循环?因为醒来后,可能已经有别的孩子把煎饼吃掉了。
for k.pancakes == 0 {
// 3. 等待
// 原子操作:
// A. 解锁互斥锁(放下钥匙)
// B. 挂起执行(入睡)
// C. 被唤醒时重新锁定互斥锁(重新拿钥匙)
k.cond.Wait()
}
// 4. 吃
k.pancakes--
fmt.Printf("Child %d ate a pancake.\n", id)
}
为什么 Wait 需要锁
如果 Wait 不释放锁,goroutine 会在 保持厨房门关闭 的状态下入睡。厨师将永远无法进入厨房制作食物。sync.Cond.Wait() 实现了一个“魔术”:它暂时放弃锁,睡眠,然后在被信号唤醒时重新获取锁。
何时使用 sync.Cond
- 多个读取者 – 许多 goroutine 等待同一个信号。
- 基于状态的等待 – 需要等到某种条件成立,例如 “缓冲区已满” 或 “服务器已就绪”,而不仅仅是等待一个值。
- 高频信号 – 想避免反复创建和关闭通道的开销。
| 原语 | 主要用途 |
|---|---|
| 通道 | 传递数据(邮递员) |
| 互斥锁 | 保护数据(锁与钥匙) |
| 条件变量 | 通知状态变化(晚餐铃) |
TL;DR
sync.Cond 并不是通道的替代品;当你需要高效地广播状态变化时,它是对通道的补充。掌握它可以让你超越 “不要通过共享内存进行通信” 的限制,帮助你为不同场景挑选合适的工具。
感谢阅读!如果你有关于 sync.Cond 的战斗经验或死锁案例,欢迎在评论区分享。