为什么 Go 函数中的 go func 需要 waitgroup 才能正确退出?

Why go func in Go function needs waitgroup to exit correctly?

对不起,这个标题可能具有误导性。实际上完整的代码如下:

package main

import (
    "fmt"
    "sync"
)

type Button struct {
    Clicked *sync.Cond
}

func main() {
    button := Button{
        Clicked: sync.NewCond(&sync.Mutex{}),
    }
    subscribe := func(c *sync.Cond, fn func()) {
        var wg sync.WaitGroup
        wg.Add(1)
        go func() {
            wg.Done()
            c.L.Lock()
            defer c.L.Unlock()
            c.Wait()
            fn()
        }()
        wg.Wait()
    }

    var clickRegistered sync.WaitGroup
    clickRegistered.Add(2)

    subscribe(button.Clicked, func() {
        fmt.Println("maximizing window")
        clickRegistered.Done()
    })
    subscribe(button.Clicked, func() {
        fmt.Println("displaying dialog")
        clickRegistered.Done()
    })

    button.Clicked.Broadcast()
    clickRegistered.Wait()
}

当我评论某些行并再次 运行 时,它会抛出一个致命错误 "all goroutines are asleep - deadlock!"
修改后的订阅函数如下所示:

subscribe := func(c *sync.Cond, fn func()) {
        //var wg sync.WaitGroup
        //wg.Add(1)
        go func() {
            //wg.Done()
            c.L.Lock()
            defer c.L.Unlock()
            c.Wait()
            fn()
        }()
        //wg.Wait()
    }

让我困惑的是go func是否在外subscribe函数returns之前执行。在我看来,go func 将 运行 作为守护进程,尽管外部函数已经返回,所以 wg 变量是不必要的。但这表明我完全错了。那么如果go func有不被调度的可能,是否意味着我们必须在每个函数或代码块中使用sync.WaitGroup来确保goroutine被调度在函数或代码之前执行阻止 returns?
谢谢大家。

使用 wg 等待组(如当前组中的编码):当 subscribe 函数 returns 时,您 知道 waiting goroutine 至少已经开始执行了。

因此,当您的主函数到达 button.Clicked.Broadcast() 时,两个 goroutine 很有可能实际上正在等待它们的 button.Clicked.Wait() 调用。

没有 wg,您无法保证 goroutine 已经启动,您的代码可能会过早调用 button.Clicked.Broadcast()


请注意,您使用 wg 只是降低了发生死锁的可能性,但并不能在所有情况下都阻止死锁。

尝试用 -race 和 运行 循环编译你的二进制文件(例如来自 bash : for i in {1..100}; do ./myprogram; done),我想你会看到相同的问题有时会发生。

问题是c.Wait()在任何一个调用中都不能保证在button.Clicked.Broadcast()之前运行;甚至你的原始代码使用 WaitGroup 也不能保证它(因为它是 c.Wait() 部分,而不是重要的 goroutine 的产生)

修改订阅:

subscribe := func(c *sync.Cond, subWG *sync.WaitGroup, fn func()) {
        go func() {
            c.L.Lock()
            defer c.L.Unlock()
            subWG.Done() // [2]
            c.Wait()
            fn()
        }()
    }

等待代码:

subWG.Done()
button.Clicked.L.Lock()
button.Clicked.L.Unlock()

这是基于以下观察,即 [2] 只能在开始时或执行 [2] 的所有先前 goroutine 都持有 c.Wait 之后发生,因为储物柜他们分享。所以subWG.Wait(),意思是2(或者订阅数)[2]被执行,只有一个goroutine没有hold住c.Wait,可以解决下次再向 Lock 索取储物柜。

游乐场:https://play.golang.org/p/6mjUEcn3ec5