对 goroutine 中的 defer 感到困惑

Confused about defer in goroutines

我发现了以下代码片段,它演示了 sync.Cond 中的 'broadcast' 功能。片段如下:

package main

import (
    "fmt"
    "sync"
)

func main() {
    type Button struct {
        Clicked *sync.Cond
    }
    button := Button{Clicked: sync.NewCond(&sync.Mutex{})}

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

    var clickRegistered sync.WaitGroup
    clickRegistered.Add(3)
    subscribe(button.Clicked, func() {
        fmt.Println("Maximizing window.")
        clickRegistered.Done()
    })
    subscribe(button.Clicked, func() {
        fmt.Println("Displaying annoying dialogue box!")
        clickRegistered.Done()
    })
    subscribe(button.Clicked, func() {
        fmt.Println("Mouse clicked.")
        clickRegistered.Done()
    })

    button.Clicked.Broadcast()

    clickRegistered.Wait()
}

输出结果如下:

Mouse clicked.
Maximizing window.
Displaying annoying dialogue box!

我更改了订阅中的 goroutine,以在 goroutine 执行完成后延迟对 gorroutineRunning 等待组的 'Done' 调用。我的想法是 waitgroup 应该只在 go routine 执行完毕后递减。所以我改了代码如下:

package main

import (
    "fmt"
    "sync"
)

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

    ....
}

随着延迟的增加,我得到了以下恐慌:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc0000b6028)
        /usr/local/go/src/runtime/sema.go:56 +0x42
sync.(*WaitGroup).Wait(0xc0000b6020)
        /usr/local/go/src/sync/waitgroup.go:130 +0x64
main.main.func1(0xc00009e040, 0xc0000b4030)
        /Users/go/concur/button.go:24 +0x91
main.main()
        /Users/go/concur/button.go:29 +0xf4

goroutine 18 [sync.Cond.Wait]:
runtime.goparkunlock(...)
        /usr/local/go/src/runtime/proc.go:310
sync.runtime_notifyListWait(0xc00009e050, 0x0)
        /usr/local/go/src/runtime/sema.go:510 +0xf8
sync.(*Cond).Wait(0xc00009e040)
        /usr/local/go/src/sync/cond.go:56 +0x9d
main.main.func1.1(0xc0000b6020, 0xc00009e040, 0xc0000b4030)
        /Users/go/concur/button.go:21 +0xbb
created by main.main.func1
        /Users/go/concur/button.go:17 +0x83
exit status 2

有人可以告诉我为什么添加延迟会导致代码崩溃吗?

原代码在 goroutine 启动后立即释放等待组 运行。当subscribe函数returns时,goroutine是存活的

当您将其更改为 defer goroutineRunning.Done() 时,goroutine 开始,并在 c.Wait() 处停止,因为它正在等待条件变量广播。由于 goroutine 正在等待 goroutineRunning.Done 未被调用,并且 subscribe 函数在 goroutineRunning.Wait 处停止。所以你第一次调用 subscribe 时,它​​会创建一个等待 cond 的 goroutine,subscribe 本身开始等待 waitgroup。有 goroutines(主要的和由 subscribe 启动的那个),都在等待某个事件发生,但是没有其他 goroutines 运行 使该事件发生,所以死锁。

我认为您认为 goroutineRunning WaitGroup 的使用存在问题是正确的。

我认为原始代码不包括 WaitGroup 但是编写代码的人发现在 3 个 go-routines称为等待()。使用 goroutineRunning 试图解决这个问题,但它并不能避免竞争条件,只会降低它发生的可能性。例如,如果你在 goroutineRunning.Done() 之后睡眠,那么你会遇到同样的问题 - 在 3 个 go-routines 等待之前调用 Broadcast()。

回到你最初的问题......向前移动 goroutine.Done()(Wait 调用之后的任何地方)将导致死锁,因为 c.Wait() 调用必须等待Broadcast() 永远不会出现,因为第一个 subscribe() 永远不会 return 直到 goroutineRunning.Done() 被调用(解除阻塞 goroutineRunning.Wait())。

将 goroutineRunning.Done() 移动到 c.Wait() 之前更好,但不会消除竞争。

要修复原始代码,您需要在调用 c.L.Lock() 之后放置 gorutineRunning.Done() 并锁定 Broadcast()。

    type Button struct {
        Clicked *sync.Cond
    }
    button := Button{ Clicked: sync.NewCond(&sync.Mutex{}) }

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

    var clickRegistered sync.WaitGroup
    clickRegistered.Add(3)
    subscribe(button.Clicked, func() {
        fmt.Println("Maximizing window.")
        clickRegistered.Done()
    })
    subscribe(button.Clicked, func() {
        fmt.Println("Displaying annoying dialog box!")
        clickRegistered.Done()
    })
    subscribe(button.Clicked, func() {
        fmt.Println("Mouse clicked.")
        clickRegistered.Done()
    })

    button.Clicked.L.Lock()     // *** new
    button.Clicked.Broadcast()
    button.Clicked.L.Unlock()   // *** new

    clickRegistered.Wait()

[顺便说一句,我承认这不是您最初问题的答案(其他答案应该得到分数),但我认为这值得一提。]