sync.cond.wait会卡死是因为调用前未持有关联锁,且唤醒后不会自动重新加锁,必须手动加锁并循环检查条件,否则导致竞态或panic;signal唤醒一个goroutine,broadcast唤醒全部,需按业务语义选择。

为什么直接用 sync.Cond.Wait 会卡死?
因为 sync.Cond.Wait 必须在持有其关联的 sync.Locker(通常是 sync.Mutex)时调用,且会自动释放锁并挂起 goroutine;但唤醒后**不会自动重新加锁**——你得自己在回调里重新获取锁,否则后续逻辑可能竞态。很多人漏写这一步,导致唤醒后读写共享变量出错,甚至 panic。
- 错误写法:
cond.Wait()后直接访问共享状态,没加锁 - 正确流程:唤醒 → 重新 lock → 检查条件 → 解锁 → 继续
- 典型现象:goroutine 唤醒后读到脏数据、panic: "sync: inconsistent mutex state"
sync.Cond.Signal 和 sync.Cond.Broadcast 的实际区别
Signal 只唤醒一个等待中的 goroutine,Broadcast 唤醒所有。但关键不是“唤醒多少”,而是**唤醒时机和条件重检是否匹配业务语义**。比如实现“生产者通知所有消费者有新批次数据”,必须用 Broadcast;但如果只是通知“某条任务完成”,用 Signal 更省资源。
-
Signal不保证唤醒哪个 goroutine,无序,不可依赖顺序 -
Broadcast会把所有阻塞在Wait的 goroutine 全部移出等待队列,但它们仍需竞争锁,再逐个检查条件是否满足(即要用 for 循环包裹Wait) - 误用
Signal替代Broadcast是常见 bug:只唤醒一个消费者,其余永远阻塞
写一个安全的阻塞等待函数:带超时 + 条件重检 + 广播兼容
核心是封装成函数,隐藏锁管理细节,强制用户以 for 循环形式提供条件判断,并支持 context.Context 超时控制。不要试图在函数内部做“一次 Wait 就返回”,那违背 cond 设计哲学。
func WaitUntil(ctx context.Context, cond *sync.Cond, condition func() bool) error {
for !condition() {
cond.L.Lock()
select {
case
- 必须用
for !condition()循环,防止虚假唤醒(spurious wakeup) -
cond.L.Unlock()在select后立即执行,避免锁持有时间过长 - 不能在
cond.Wait()前加defer cond.L.Unlock()——Wait会自己 unlock,defer 会导致 double unlock panic - 若要支持广播唤醒,调用方在修改共享状态后,必须显式调用
cond.Broadcast()
为什么不能把 sync.Cond 和状态变量封装进 struct 自动管理?
可以封装,但容易掉进“隐藏唤醒逻辑”的坑。比如把 cond.Broadcast() 写在 setter 里,看似方便,实则破坏职责分离:状态变更 ≠ 总该广播,有些变更只是内部调整,不该唤醒等待者。
- 典型反模式:
SetReady(true)内部自动Broadcast,但调用方其实只想标记状态,不希望触发等待逻辑 - 正确做法:状态变更和唤醒解耦,由业务逻辑决定何时
Broadcast - 真正该封装的是“等待协议”,比如
WaitForReady、WaitForCount这类语义化函数,而不是把 cond 和字段绑死
复杂点从来不在 cond 本身,而在于谁负责判断“现在该不该唤醒”——这个决策必须留在业务层,不能交给并发原语代劳。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











