登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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,生产者也可能在它检查之后、真正进入等待之前修改状态,最终让“已经满足的条件”没有被正确消费。

Go sync.Cond 中 Cond.L、共享条件、Wait 与唤醒通知的静态锁边界说明图
图1:结构说明图,展示 Cond.L、共享条件、Wait 和通知之间的静态关系,不是运行截图。

为什么 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,本质上是在每次真正使用共享数据前重新建立不变量。

Go sync.Cond 中共享条件、等待循环、Signal 与资源槽位的静态关系说明图
图2:关系说明图,展示条件判断、等待循环、Signal 和槽位状态的边界,不是运行结果。

状态修改和通知为什么要放在同一把锁附近

生产者可以在持锁时调用 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。工程上更重要的是,条件修改与条件读取必须由同一把锁保护,保持状态转移容易理解。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>