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

Go语言WaitGroup疑问:中途调用Done()及defer死锁解析

关于sync.WaitGroup的两个疑问解答

问题1:goroutine中途调用wg.Done()的原因

我理解wg.Done()用于向WaitGroup告知goroutine已完成,使wg.Wait()结束,通常会用defer确保在goroutine末尾调用。但遇到如下代码在goroutine中途调用wg.Done(),且后续代码仍在执行,对此感到困惑,想了解这样做的原因:

func startNetworkDaemon() *sync.WaitGroup {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        connPool := warmServiceConnCache()

        server, err := net.Listen("tcp", "localhost:8080")
        if err != nil {
            log.Fatalf("cannot listen: %v", err)
        }
        defer server.Close()

        wg.Done()

        for {
            conn, err := server.Accept()
            if err != nil {
                log.Printf("cannot accept connection: %v", err)
                continue
            }
            svcConn := connPool.Get()
            fmt.Fprintln(conn, "")
            connPool.Put(svcConn)
            conn.Close()
        }
    }()
    return &wg
}

解答

这段代码里的WaitGroup核心作用是等待网络服务初始化完成,而非等待整个goroutine生命周期结束:

  • warmServiceConnCache()预热连接池、net.Listen()启动TCP监听,这两步是服务启动的关键初始化操作,必须确保完成后,外部调用startNetworkDaemon()返回的WaitGroup的wg.Wait()才能结束,以此告知调用方:服务已经准备好接受客户端请求了。
  • 后续的for循环是服务持续处理连接的主逻辑,会一直运行到服务关闭。如果把wg.Done()放在goroutine末尾,wg.Wait()永远无法等到结束信号(因为循环不会主动退出),也就无法向外部传达服务就绪的状态。所以提前在初始化完成后调用wg.Done(),既能让外部感知服务就绪,又不影响goroutine继续处理后续请求。

问题2:给wg.Done()添加defer触发死锁的原因

另一段代码中,若给wg.Done()添加defer会触发死锁,想请解释该死锁的原因:

func usingBroadcast() {

  beeper := sync.NewCond(&sync.Mutex{})
  var wg sync.WaitGroup
  wg.Add(1)

  miniFunc(func() {
    fmt.Println("mini1")  
    wg.Done() 
  }, beeper) 

  beeper.Broadcast() 
  wg.Wait()
  
}

func miniFunc(fn func(), beeper *sync.Cond) {

    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
      wg.Done()
      beeper.L.Lock()
      fmt.Println("i am waiting")
      beeper.Wait()
      beeper.L.Unlock()
      fn()
      
    }()

    wg.Wait()
}

解答

先看修改后触发死锁的代码(将wg.Done()改为defer wg.Done()):

func miniFunc(fn func(), beeper *sync.Cond) {
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
      defer wg.Done() // 此处修改为defer
      beeper.L.Lock()
      fmt.Println("i am waiting")
      beeper.Wait()
      beeper.L.Unlock()
      fn()
    }()
    wg.Wait()
}

死锁的连锁逻辑如下:

  1. miniFunc启动goroutine后,goroutine先执行beeper.L.Lock()获取条件变量的锁,接着调用beeper.Wait()——这个方法会自动释放锁并阻塞,等待外部的Broadcast信号。但此时defer wg.Done()还未执行,因为goroutine还没有退出。
  2. miniFunc中的wg.Wait()会一直阻塞,等待goroutine调用wg.Done(),但goroutine卡在beeper.Wait()处,没有机会执行到defer wg.Done()。
  3. 回到usingBroadcast()函数,miniFunc()因内部的wg.Wait()阻塞,导致后续的beeper.Broadcast()根本无法执行;没有广播信号,goroutine就会一直卡在beeper.Wait(),miniFunc持续阻塞,usingBroadcast里的wg.Wait()也永远等不到fn()中的wg.Done(),最终整个程序陷入死锁。

而原代码中,goroutine先调用wg.Done(),再执行加锁等待逻辑,miniFunc的wg.Wait()能正常结束,usingBroadcast()里的beeper.Broadcast()得以执行,唤醒等待的goroutine后,fn()才会被调用,整个流程无阻塞。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 16:35:24