You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go中Cond场景下fmt.Println置于wg.Done后引发死锁的原因咨询

Go语言发布订阅模式下的偶发死锁原因分析

问题场景

以下是一段采用发布订阅模式的Go代码,当把fmt.Println("waiting")放在goroutineRunning.Done()之后时,会偶发死锁;移除该语句或把它移到Done()之前则运行正常,死锁需要多次执行go run main.go才会触发:

package main

import (
    "fmt"
    "sync"
)

func main() {
    cond := sync.NewCond(&sync.Mutex{})

    subscribe := func(c *sync.Cond, fn func()) {
        var goroutineRunning sync.WaitGroup
        goroutineRunning.Add(1)

        go func() {
            goroutineRunning.Done()
            fmt.Println("waiting")

            c.L.Lock()
            c.Wait()
            c.L.Unlock()

            fn()
        }()

        goroutineRunning.Wait()
    }

    var clickRegistered sync.WaitGroup
    clickRegistered.Add(2)

    subscribe(cond, func() {
        fmt.Println("notified 1")
        clickRegistered.Done()
    })

    subscribe(cond, func() {
        fmt.Println("notified 2")
        clickRegistered.Done()
    })

    cond.Broadcast()
    clickRegistered.Wait()
}

死锁原因解析

核心问题在于条件变量的广播时机和goroutine进入等待状态的时机不匹配,具体过程如下:

  1. 当goroutineRunning.Done()先执行时,主goroutine里的goroutineRunning.Wait()会立刻返回,继续执行后续代码(比如第二个subscribe,或者直接走到cond.Broadcast())。
  2. 订阅goroutine在调用Done()后,还要执行fmt.Println("waiting")——这个IO操作会消耗少量时间,同时Go调度器可能在这个节点切换回主goroutine。
  3. 如果主goroutine在某个订阅goroutine还没执行到c.Wait()的时候,就调用了cond.Broadcast(),那么这个订阅goroutine后续再调用c.Wait()时会永远阻塞:因为条件变量的广播是一次性的,错过这次广播后,没有其他通知能唤醒它。
  4. 被阻塞的订阅goroutine永远不会执行fn(),也就不会调用clickRegistered.Done(),主goroutine的clickRegistered.Wait()会一直等待,最终导致整个程序死锁。

而把fmt.Println("waiting")移到goroutineRunning.Done()之前,或者移除它时,订阅goroutine会更快地进入c.Wait()状态,主goroutine的goroutineRunning.Wait()返回时,订阅goroutine大概率已经处于等待状态,此时cond.Broadcast()能正确唤醒所有等待的goroutine,不会出现死锁。

内容的提问来源于stack exchange,提问作者Hari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 17:01:10