文章目录

Go 条件变量:sync.Cond 与等待通知

发布于 2026-04-02 10:26:18 · 浏览 7 次 · 评论 0 条

Go 条件变量:sync.Cond 与等待通知

Go 语言的 sync 包提供了多种同步原语,其中 sync.Cond 是一个用于协调 goroutine 之间“等待-通知”行为的条件变量。它常用于解决多个 goroutine 需要等待某个共享状态发生变化后再继续执行的问题。


何时使用 sync.Cond?

不要在能用 channel 解决问题时强行使用 sync.Cond。但当你遇到以下情况时,sync.Cond 是更合适的选择:

  • 多个 goroutine 同时等待同一个条件成立(例如缓冲区非空、任务队列有新任务)。
  • 条件的变化由另一个 goroutine 主动触发通知
  • 不希望为每个等待者创建单独的 channel,避免资源浪费或逻辑复杂。

sync.Cond 的核心机制是:goroutine 调用 Wait() 进入阻塞状态,直到另一个 goroutine 调用 Signal()Broadcast() 唤醒它。


创建 sync.Cond

构造一个 sync.Cond 实例需要传入一个 *sync.Locker(通常是 *sync.Mutex*sync.RWMutex):

var mu sync.Mutex
cond := sync.NewCond(&mu)

这个锁的作用是:保护共享状态,确保在检查条件和进入等待之间不会被其他 goroutine 干扰。


基本使用模式

使用 sync.Cond 的标准流程如下:

  1. 获取锁(调用 Lock())。
  2. 检查条件是否满足。如果不满足,调用 Wait()
  3. Wait() 内部会自动释放锁,使其他 goroutine 有机会修改状态。
  4. 当被唤醒后,Wait() 重新获取锁,然后返回。
  5. 再次检查条件(因为可能被虚假唤醒),决定是否继续执行。
  6. 释放锁(调用 Unlock())。

下面是一个典型示例:一个生产者不断向队列添加任务,多个消费者等待任务到来。

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    queue := make([]int, 0)

    // 启动 3 个消费者
    for i := 0; i < 3; i++ {
        go func(id int) {
            for {
                mu.Lock()
                // 循环检查条件:队列为空则等待
                for len(queue) == 0 {
                    cond.Wait()
                }
                // 取出第一个任务
                task := queue[0]
                queue = queue[1:]
                mu.Unlock()
                fmt.Printf("Consumer %d processing task %d\n", id, task)
                time.Sleep(100 * time.Millisecond) // 模拟处理时间
            }
        }(i)
    }

    // 生产者:每秒添加一个任务
    for i := 1; ; i++ {
        mu.Lock()
        queue = append(queue, i)
        mu.Unlock()
        cond.Signal() // 唤醒一个等待的消费者
        time.Sleep(time.Second)
    }
}

注意几个关键点:

  • 必须用 for 循环检查条件,而不是 if。因为 Wait() 可能被虚假唤醒(spurious wakeup)。
  • Signal() 只唤醒一个等待者;如果想唤醒所有等待者,使用 Broadcast()
  • 所有对共享状态(如 queue)的访问都必须在持有锁的情况下进行

Signal() vs Broadcast()

方法 行为 适用场景
Signal() 唤醒一个正在 Wait() 的 goroutine 通常用于“一个任务唤醒一个消费者”的场景
Broadcast() 唤醒所有正在 Wait() 的 goroutine 用于“状态全局变化”,所有等待者都需要重新评估条件

例如,在缓存失效场景中,当缓存被清空后,所有等待缓存更新的 goroutine 都应被唤醒,此时应使用 Broadcast()


常见错误与陷阱

  1. 忘记在循环中检查条件
    使用 if 而不是 for 检查条件,可能导致 goroutine 在虚假唤醒后错误地继续执行。

  2. 未持有锁就调用 Wait()/Signal()/Broadcast()
    这些方法必须在持有与 Cond 关联的锁时调用,否则会导致 panic。

  3. 在 Wait() 返回后不重新验证条件
    即使被唤醒,条件也可能不再成立(例如被其他 goroutine 抢先消费),因此必须再次检查。

  4. 滥用 Broadcast() 导致性能下降
    唤醒所有等待者会引发“惊群效应”(thundering herd),不必要的 goroutine 被唤醒后又立即进入等待,浪费 CPU。


与 channel 的对比

虽然 channel 也能实现类似功能,但各有优劣:

  • channel 更适合“一对一”或“多对一”的消息传递
  • sync.Cond 更适合“多对多”的状态等待,尤其是当等待条件复杂或状态变化频繁时。
  • 使用 channel 模拟多个消费者等待同一条件,通常需要额外的 goroutine 转发消息,增加复杂度。

例如,若用 channel 实现上述消费者模型,可能需要一个广播 channel 或 fan-out 模式,而 sync.Cond 直接在共享状态上操作,逻辑更清晰。


性能注意事项

  • sync.Cond 的内部实现基于 runtime_Semacquireruntime_Semrelease,底层使用 futex(Linux)等高效同步机制。
  • 但在高并发场景下,频繁调用 Broadcast() 可能导致大量 goroutine 竞争锁,反而降低性能。
  • 如果等待者数量固定且较少,优先考虑 channel;如果等待者动态增减或数量庞大,sync.Cond 更合适。

实战:实现一个简单的 Barrier

Barrier 是一种同步原语,要求 N 个 goroutine 都到达某一点后才能继续执行。我们可以用 sync.Cond 实现:

type Barrier struct {
    mu      sync.Mutex
    cond    *sync.Cond
    count   int
    waiting int
}

func NewBarrier(n int) *Barrier {
    b := &Barrier{count: n}
    b.cond = sync.NewCond(&b.mu)
    return b
}

func (b *Barrier) Wait() {
    b.mu.Lock()
    b.waiting++
    if b.waiting < b.count {
        b.cond.Wait()
    } else {
        b.cond.Broadcast()
    }
    b.mu.Unlock()
}

使用方式:

barrier := NewBarrier(3)
for i := 0; i < 3; i++ {
    go func(id int) {
        fmt.Printf("Goroutine %d ready\n", id)
        barrier.Wait()
        fmt.Printf("Goroutine %d proceeding\n", id)
    }(i)
}

此实现展示了 sync.Cond 如何协调多个 goroutine 的同步点。


始终确保:在调用 Wait() 前持有锁,在 Wait() 返回后重新验证条件,并在适当时候调用 Signal()Broadcast()。正确使用 sync.Cond 能让你在复杂的并发控制中游刃有余。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文