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?
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.