Search code examples
deadlockgoroutine

goroutine deadlock with waitgroup


I was reading through the Go concurrency book, and I came across the following example.

func condVarBroadcast() {
        type button struct {
                clicked *sync.Cond
        }
        b := button{sync.NewCond(&sync.Mutex{})}

        // for each call to subscribe, start up a new goroutine
        // and wait using the condition variable.
        subscribe := func(c *sync.Cond, fn func()) {
                var w sync.WaitGroup
                w.Add(1)
                go func() {
                        w.Done()
                        c.L.Lock()
                        defer c.L.Unlock()
                        c.Wait()
                        fn()
                }()
                w.Wait()
        }

        var wg sync.WaitGroup
        wg.Add(3)
        subscribe(b.clicked, func() {
                fmt.Println("Maximize window")
                wg.Done()
        })
        subscribe(b.clicked, func() {
                fmt.Println("displaying dialog box")
                wg.Done()
        })
        subscribe(b.clicked, func() {
                fmt.Println("mouse clicked")
                wg.Done()
        })

        b.clicked.Broadcast()
        wg.Wait()
}

I had a couple of doubts:

  • if I replace "w.Done()" with "defer w.Done()", there is a deadlock. Why? What is the point of doing w.Done() inside a goroutine when it clearly is not done executing? Is this a normal practice?
  • Why do we even need to waitgroup w in this example? If I don't use it, it says "deadlock: all goroutines are asleep.

Solution

  • Here the problem with race condition is that the main is super fast and broadcasts to the sync.Cond even before the go-routines start and wait for the lock. The reason to have w waitgroup is to make sure that all the go-routines start first and hold the lock before the main thread signals the broadcast

    func condVarBroadcast() {
            type button struct {
                    clicked *sync.Cond
            }
            b := button{sync.NewCond(&sync.Mutex{})}
    
            // for each call to subscribe, start up a new goroutine
            // and wait using the condition variable.
            subscribe := func(c *sync.Cond, fn func()) {
                    var w sync.WaitGroup
                    w.Add(1)
                    go func() {
                            w.Done() // <-------- this will notify that the goroutine has started
                            c.L.Lock()
                            defer c.L.Unlock()
                            c.Wait()
                            fn()
                    }()
                    w.Wait() // <--- this will block till the go routine starts and then continue
            }
    
            var wg sync.WaitGroup
            wg.Add(3)
            subscribe(b.clicked, func() {
                    fmt.Println("Maximize window")
                    wg.Done()
            })    //    < ----- This call will block till the go routine starts
            subscribe(b.clicked, func() {
                    fmt.Println("displaying dialog box")
                    wg.Done()
            }) //    < ----- This call will block till the go routine starts
            subscribe(b.clicked, func() {
                    fmt.Println("mouse clicked")
                    wg.Done()
            }) //    < ----- This call will block till the go routine starts
    
            b.clicked.Broadcast() // <------------ Once this happens all the go routines will continue the processing
            wg.Wait()
    }
    

    If you want to avoid the w waitgroup, you have to somehow make sure the Broadcast call happens after all the go routines start in that case you could add a simple time.Sleep(time.Second * 1) before the Broadcast call and remove the w waitgroup from the code.