sync.Cond 的文档只用一句话要求 Wait 必须放在循环里。如果写成 if 会怎样?这篇用 8 个消费者复现这个 bug,量 Signal 与 Broadcast 唤醒的个数,看信号先发后等会不会丢,并和 channel 比较来回传递的耗时。
Wait 放在 if 里:3000 个元素,取走 3000 个,醒来后队列里其实没有元素、却照常取走的次数 = 2998 Wait 放在 for 循环 里:3000 个元素,取走 3000 个,醒来后队列里其实没有元素、却照常取走的次数 = 0
Broadcast 一次唤醒 8 个消费者,但只放入了 1 个元素:它们依次拿到锁,第一个取走元素,其余 7 个醒来后队列是空的。用 if 就会直接往下取,队列变成负数;之后条件(队列≠0)又被误判成立,整段逻辑失控——实验里 3000 个元素有 2998 次取走时队列其实没有元素。用 for 就是 0 次。
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。
两个 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 完全对应。