Shahrukh Mohammad
Shahrukh Mohammad

Reputation: 1105

Channel synchronisation with WaitGroup. Closing channel and Waitgroup

I am trying to understand synchronisation in Goroutines. I have a code here that writes numbers from 0 to 4 on a channel and once done I read from the channel using range and print the values.

The below code works fine where I am waiting using wg.Wait() and closing the channel in a separate Goroutine.

package main

import (
    "fmt"
    "strconv"
    "sync"
)

func putvalue(i chan string, value string, wg *sync.WaitGroup) {
    i <- value
    defer wg.Done()
}

func main() {
    queue := make(chan string)
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go putvalue(queue, strconv.Itoa(i), &wg)
    }

    go func() {
        wg.Wait()
        close(queue)
    }()

    for elem := range queue {
        fmt.Println(elem)
    }

}

https://play.golang.org/p/OtaRP3Mm4lk

But if I use the exact same code, but wait and close the channel in the main thread, this results in a deadlock. The below code results in deadlock.

package main

import (
    "fmt"
    "strconv"
    "sync"
)

func putvalue(i chan string, value string, wg *sync.WaitGroup) {
    i <- value
    defer wg.Done()
}

func main() {
    queue := make(chan string)
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)
        go putvalue(queue, strconv.Itoa(i), &wg)
    }

    wg.Wait()
    close(queue)

    for elem := range queue {
        fmt.Println(elem)
    }

}

https://play.golang.org/p/JXmdsdPKQPu

From what I can understand that in the second case the main thread execution stops and waits, but how is it different from doing it in a separate goroutine? Please help me understand this.

Upvotes: 4

Views: 4845

Answers (2)

torek
torek

Reputation: 488213

It may help to think of each goroutine as a separate person (or gopher: https://blog.golang.org/gopher). When you go f() you get a new person/gopher and give them the job of running the function. So, you have 5 extra gophers that are running this:

func putvalue(i chan string, value string, wg *sync.WaitGroup) {
    i <- value
    defer wg.Done()
}

Each one of the 5 runs up to the point where they reach the i <- value line, and then they stop, waiting for a gopher to run up to the "get" side of the channel / mailbox and stick his hands through to get a string, which is the kind of package that goes into the i channel / mailbox.

(Aside: the defer wg.Done() should be the first line of the function, not the last. Or, just do wg.Done() as the last line of the function.)

Now, if at this point you acquire a sixth extra gopher and make him do:

{
    wg.Wait()
    close(queue)
}

he'll stop inside wg.Wait(), waiting.

Your main gopher now continues on to the for loop. This reads from the mailbox, i.e., now your main gopher sticks his hands into the mailbox/window, on the "get" side of the channel. One of the five waiting gophers can finally put his string into your gopher's hands. Your main Gopher takes the string and brings it back to your for loop. The one of the five gophers that were blocked can now execute his wg.Done() and expire (presumably off to a happy land of retired gophers 😀).

Your main gopher continues in the for loop, getting more packages through the mailbox. As he does so, the four gophers that are waiting on the mailbox "put" finish up and call wg.Done(), which counts the workgroup counter down. When the count reaches zero, there are no gophers waiting to put packages into the mailbox any more, but now the gopher that was asleep, waiting for wg.Wait(), is awakened. So he'll soon wake up and call close.

If he hasn't called close yet, your main gopher is stuck waiting for the next package. So only the one remaining gopher can do anything: he'll do the close. Or, maybe he woke up quickly and already did the close, but if so, your main gopher has already seen that the mailbox window is closed forever. Either way, your main gopher will already have seen, or will be about to see, that the mailbox window—the channel—is closed, and the for loop will stop and your main gopher will return from main and also head off to happy-retirement-land.

As Burak Serdar noted, though, without a separate gopher doing the wg.Wait() followed by close, it's your main gopher that is doing the wg.Wait(). So he never gets around to the for loop to read from the (still open) mailbox / channel. Your five gophers are asleep, waiting for a gopher to put his hands through the mailbox to get their packages. Your main gopher is asleep, waiting for the counter in the sync.WaitGroup to go down to zero. Everyone's asleep!

Upvotes: 8

Burak Serdar
Burak Serdar

Reputation: 51587

In the first program, the main goroutine creates several goroutines where each goroutine start waiting on channel write. Then the main goroutine creates another goroutine that waits on wg.Wait(). The main goroutine continues, reads from the channels, which also enables all goroutines one by one, and then they terminate, releasing the goroutine waiting on wg.Wait().

In the second program, you again create goroutines that wait on channel write, but this time, main goroutine calls wg.Wait(). At this point, all goroutines you created are waiting for channel to become writable, and the main goroutine is waiting for the goroutines to end, which means deadlock

Upvotes: 1

Related Questions