Search code examples
gosynchronizationdeadlockgoroutine

Confused about defer in goroutines


I came across the following code snippet that demonstrates the 'broadcast' functionality in sync.Cond. The snippet is as follows:

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()
}

The output is as follows:

Mouse clicked.
Maximizing window.
Displaying annoying dialogue box!

I changed the goroutine within the subscribe to defer the calling of 'Done' on the gorroutineRunning waitgroup after the goroutine is done executing. My thinking was that the waitgroup should decrement only after the go routine is done executing. So I changed the code as follows:

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()
    }

    ....
}

With the addition of the defer I get the following panic:

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

Can somebody walk me through why adding the defer is causing the code to panic?


Solution

  • The original code releases the waitgroup as soon as the goroutine starts running. When the subscribe function returns, the goroutine is alive.

    When you changed that to defer goroutineRunning.Done(), the goroutine starts, and stops at c.Wait(), because it is waiting for the condition variable broadcast. Since the goroutine is waiting there the goroutineRunning.Done is not called, and subscribe functions stops at the goroutineRunning.Wait. So the first time you call subscribe, it creates a goroutine that's waiting on a cond, subscribe itself starts waiting on the waitgroup. There are to goroutines (main and the one started by subscribe), both waiting for some event to happen, but there are no other goroutines running to make that event happen, so deadlock.