bigywater.tech
Go · 77

sync.Cond:Wait 为什么必须放在循环里

sync.Cond 的文档只用一句话要求 Wait 必须放在循环里。如果写成 if 会怎样?这篇用 8 个消费者复现这个 bug,量 Signal 与 Broadcast 唤醒的个数,看信号先发后等会不会丢,并和 channel 比较来回传递的耗时。

2998 / 0
Wait 放 if 里和放 for 里,醒来后队列为空却照常取走的次数(3000 个元素)
1 vs 8
8 个 goroutine 在 Wait 时,Signal 与 Broadcast 唤醒的个数
308 vs 261 ns
来回传递:sync.Cond 对无缓冲 channel

if 为什么错

实测 · 本机执行输出
  Wait 放在 if 里:3000 个元素,取走 3000 个,醒来后队列里其实没有元素、却照常取走的次数 = 2998
  Wait 放在 for 循环 里:3000 个元素,取走 3000 个,醒来后队列里其实没有元素、却照常取走的次数 = 0

Broadcast 一次唤醒 8 个消费者,但只放入了 1 个元素:它们依次拿到锁,第一个取走元素,其余 7 个醒来后队列是空的。用 if 就会直接往下取,队列变成负数;之后条件(队列≠0)又被误判成立,整段逻辑失控——实验里 3000 个元素有 2998 次取走时队列其实没有元素。用 for 就是 0 次。

Signal 与 Broadcast

实测 · 本机执行输出
  8 个 goroutine 在 Wait,放入 1 个元素后调用 Signal:从 Wait 返回的有 1 个
  8 个 goroutine 在 Wait,放入 1 个元素后调用 Broadcast:从 Wait 返回的有 8 个

Signal 唤醒 1 个,Broadcast 唤醒全部 8 个。后者在等待者多、每次只放一个资源时是惊群,代价是 7 个无谓的唤醒和抢锁。

信号不排队

实测 · 本机执行输出
  Signal 先于 Wait 调用:300 ms 后消费者仍卡在 Wait(信号丢失了)

没人在 Wait 时发出的 Signal 直接丢失,之后才来的 Wait 会一直等下去(实验里 300 ms 后仍卡住)。所以条件必须存在共享变量里:先在锁内检查条件,不满足再 Wait。

不比 channel 快

实测 · 本机执行输出
  两个 goroutine 来回传递 200000 次:sync.Cond 308 ns/个来回;无缓冲 channel 261 ns/个来回

两个 goroutine 来回传递 20 万次,Cond 308 ns,无缓冲 channel 261 ns。Cond 的用武之地是“多个等待者等同一个条件、可能广播”,而不是传值。

文档怎么说

实测 · 本机执行输出
  Wait atomically unlocks c.L and suspends execution of the calling goroutine.
  After later resuming execution, Wait locks c.L before returning. Unlike
  in other systems, Wait cannot return unless awoken by Cond.Broadcast or
  Cond.Signal.
  Because c.L is not locked while Wait is waiting, the caller typically cannot
  assume that the condition is true when Wait returns. Instead, the caller
  should Wait in a loop:
  c.L.Lock()
  for !condition() {
  c.Wait()
  }
  ... make use of condition ...
  c.L.Unlock()

Wait 原子地解锁并挂起,返回前重新加锁;但等待期间锁是放开的,条件可能已被别人改掉,所以文档给出的写法就是 for !condition() { c.Wait() }。这与实验里的 bug 完全对应。

分步演示

sync.Cond
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。第一节:8 个消费者、1 个生产者每隔约 50 µs 放一个元素并 Broadcast,3000 个元素;“照常取走”指消费者在队列 ≤ 0 时仍取走元素;其余各节为单独的小程序;来回传递各 20 万次,单次测量;文档引自 go doc sync.Cond.Wait。等待条件用“队列 ≤ 0”,这样 if 版本下队列变成负数也会被算作“没有元素”
    • 没有验证:真正的“虚假唤醒”(Go 的文档说 Wait 只会被 Signal/Broadcast 唤醒,本文没有试图复现别的唤醒)、Cond 的内部实现(notifyList)、Broadcast 后的唤醒顺序、大量等待者时的开销、其它架构
    京ICP备2026067502号-1京公网安备11010502063487号