解锁 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 的战斗经验或死锁案例,欢迎在评论区分享。

Back to Blog

相关文章

阅读更多 »