Go sync.Cond.Wait 为什么必须配合锁使用
来源:17golang原创
时间:2026-11-03 01:29:20 164浏览 收藏
Go 的 sync.Cond.Wait 必须在已经持有 c.L 的情况下调用。它会原子地释放这把锁并挂起当前 goroutine;被 Signal 或 Broadcast 唤醒后,返回前又会重新拿到锁。因此,Wait 不是“睡一会儿”,而是把“检查条件、释放锁、被唤醒、重新加锁”连接成一个同步边界。
官方文档:https://pkg.go.dev/sync#Cond.Wait
- 调用
Wait前必须持有Cond.L,否则条件检查和等待不在同一保护范围内。 - 被唤醒只代表共享状态可能变化,返回后必须在
for循环中重新检查条件。 - 修改条件和发送通知应使用同一把锁;一次性通知场景通常可以直接用 channel。
Wait 先释放锁,返回前再把锁拿回来
sync.Cond 绑定一个 Locker。官方约定是:观察或修改条件时持有这把锁,调用 Wait 时也必须持有它。下面的最小例子展示了消费者和生产者共享的边界:
package main
import "sync"
var (
mu sync.Mutex
cond = sync.NewCond(&mu)
ready bool
)
func waitReady() {
cond.L.Lock()
for !ready {
// Wait 会原子释放 cond.L;返回前会重新持有它。
cond.Wait()
}
// 这里仍然持有 cond.L,可以安全读取 ready 保护的数据。
cond.L.Unlock()
}
func markReady() {
cond.L.Lock()
ready = true // 先修改共享条件,再通知等待者。
cond.Broadcast()
cond.L.Unlock()
}
如果只写 cond.Wait() 而没有先 Lock,就越过了 Cond.L 规定的使用边界。更重要的是,消费者会在没有保护的情况下观察 ready,生产者也可能在它检查之后、真正进入等待之前修改状态,最终让“已经满足的条件”没有被正确消费。

为什么 Wait 返回后还要用 for 再检查一次
Signal 和 Broadcast 的职责是唤醒等待者,不是替调用者证明条件为真。等待期间锁不在当前 goroutine 手里,其他 goroutine 可能先拿锁修改状态;多个消费者也可能竞争同一份资源。因此,返回后必须重新拿锁并回到条件判断。
type Gate struct {
mu sync.Mutex
cond *sync.Cond
slots int
}
func NewGate() *Gate {
g := &Gate{}
g.cond = sync.NewCond(&g.mu)
return g
}
func (g *Gate) Take() {
g.mu.Lock()
for g.slots == 0 {
// 不能把一次唤醒当成一定有可用槽位。
g.cond.Wait()
}
g.slots-- // 重新拿锁后再消费共享状态。
g.mu.Unlock()
}
func (g *Gate) Release() {
g.mu.Lock()
g.slots++ // 状态先变为可用,再唤醒一个等待者。
g.cond.Signal()
g.mu.Unlock()
}
这里的 for 同时覆盖了两类情况:唤醒后槽位已经被另一个 goroutine 取走,以及通知发生时条件并没有按当前消费者需要的方式成立。把 if 改成 for,本质上是在每次真正使用共享数据前重新建立不变量。

状态修改和通知为什么要放在同一把锁附近
生产者可以在持锁时调用 Signal 或 Broadcast,也可以在某些场景下不持锁发送通知;但共享条件本身必须由同一同步边界保护。更容易维护的写法是:先持有 c.L,修改条件,发送通知,再解锁。这样读者能清楚看到条件的改变和唤醒动作属于同一次状态转移。
| 写法 | 问题 | 建议 |
|---|---|---|
Wait 前未持锁 | 条件检查与等待脱节 | 先 c.L.Lock() |
用 if 包住 Wait | 返回后直接相信条件 | 改为 for !condition() |
| 条件由多把锁分别保护 | 难以判断谁拥有状态 | 统一使用 Cond.L |
| 只是传一次值或广播结束 | Cond 结构偏重 | 比较 channel 的发送或关闭 |
什么时候不该继续使用 sync.Cond
sync.Cond 适合多个 goroutine 围绕一个可变条件反复等待和唤醒,例如资源槽位、队列容量或阶段状态。若需求只是把一个值传给另一个 goroutine,使用 channel 更直观;如果只是通知“以后都不要再等”,关闭 channel 也能表达一次性广播。Go 官方文档同样提示,很多简单场景优先考虑 channel。
- 看到
Wait,先问调用点是否持有Cond.L。 - 看到
Signal或Broadcast,再检查消费者是否用for重新判断条件。 - 看到多个状态变量,确认它们是否由同一把锁保护并在同一边界内更新。
常见问题
Wait 会不会自己返回?
不会。官方文档说明它需要被 Signal 或 Broadcast 唤醒;但唤醒后仍要重新判断共享条件。
Signal 一定唤醒指定的 goroutine 吗?
不会。它唤醒一个等待者,但不提供等待者之间的调度优先级保证,所以条件判断不能依赖某个固定顺序。
调用 Broadcast 时必须持锁吗?
官方允许但不要求调用者持有 c.L。工程上更重要的是,条件修改与条件读取必须由同一把锁保护,保持状态转移容易理解。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
301 收藏
-
451 收藏
-
143 收藏
-
253 收藏
-
370 收藏
-
214 收藏
-
485 收藏
-
328 收藏
-
249 收藏
-
359 收藏
-
206 收藏
-
465 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习