Buffered Channels with Goroutines using For loop

2.1k views Asked by At

I am a newbie in golang and trying to experiment buffered channels with goroutines. I thought I understood how buffered channels work with goroutines until encountered the below example which becomes a brain teaser for me and gave a bang to the concepts that I have learned so far.

This is the original example that I took from the article https://medium.com/rungo/anatomy-of-channels-in-go-concurrency-in-go-1ec336086adb.

Code#1: (channel capacity=3, channel length=3, loop length=4)

func squares(c chan int) {
    for i := 0; i <= 3; i++ {
        num := <-c
        fmt.Println(num * num)
    }
}

func main() {
    fmt.Println("main() started")
    c := make(chan int, 3)

    go squares(c)

    c <- 1
    c <- 2
    c <- 3
    
    fmt.Println("main() stopped")
}

Output:

main() started
main() stopped

Explanation: In the above program, channel c has a buffer capacity of 3. That means it can hold 3 values. Since the buffer is not overflowing (as we didn’t push any new value), the main goroutine will not block and the program exists. I have understood this example.

Code#2: (channel capacity=3, channel length=4, loop length=4)

func squares(c chan int) {
    for i := 0; i <= 3; i++ {
        num := <-c
        fmt.Println(num * num)
    }
}

func main() {
    fmt.Println("main() started")
    c := make(chan int, 3)

    go squares(c)

    c <- 1
    c <- 2
    c <- 3
    c <- 4 // goroutine blocks here
    
    fmt.Println("main() stopped")
}

Output:

main() started
1
4
9
16
main() stopped

Explanation: As now a filled buffer gets the push by c <- 4 send operation, main goroutine blocks and squares goroutine drains out all the values. It is also understood by me.

Code#3: (channel capacity=3, channel length=5, loop length=5)

func squares(c chan int) {
    for i := 0; i <= 4; i++ {
        num := <-c
        fmt.Println(num * num)
    }
}

func main() {
    fmt.Println("main() started")
    c := make(chan int, 3)

    go squares(c)

    c <- 1
    c <- 2
    c <- 3
    c <- 4 // goroutine blocks here
    c <- 5

    fmt.Println("main() stopped")
}

Output:

main() started
1
4
9
16
25
main() stopped

Explanation: I have added another value to the channel which is 5. Although the channel capacity is only 3.

I understand that until the channel receives n+1 send operations, it won’t block the current goroutine. On the value 4, it receives n+1 operations, that's why goroutine gets blocked and drains out all the values but what I am unable to understand is that how n+2 operations are dealt by channel. Is it because we have read the values from the channel and we have more space for reading?

2

There are 2 answers

2
cod3rboy On

The channel capacity is not getting full here because your squares goroutine is running and it immediately receives values that are being sent to the channel.

but what I am unable to understand is that how n+2 operations are dealt by channel.

At n+1 send operation, channel capacity is full so it will block. After at least one value is received from the channel (so a space is available to send next value) n+1 send operation continues and again capacity is full. Now at n+2 send operation, since the capacity is full so it will block until at least one value is received from channel and so on.

5
LeGEC On

You observe a certain way the scheduler orders the actions of your porgram, but the code you displayed does not guarantee that your instructions will always be executed this way.

You can try to run your program 100 times, and see if you always have the same output :

go build myprogram
for i in {1..100}; do
  ./myprogram
done

You can also turn the race detector on (one effect of the the race detector is that it introduces more randomization on the scheduler) :

go build -race myprogram
for i in {1..100}; do
  ./myprogram
done

Here are some outputs which would also be compatible with your last "5 items" example :

main() started
1
4
9
16
main() stopped
main() started
1
4
main() stopped
9
main() started
1
main() stopped

To give a more concrete view of what can be made to have "always the same behavior", here pretty standard ways in go to have your sample program run all its tasks before exiting :

  • use a sync.WaitGroup, to have the squares() function indicate it has completed its work :
func squares(c chan int, wg *sync.WaitGroup) {
    defer wg.Done()  // <- decrement the counter by 1 when
                     //    returning from this function

    for i := 0; i <= 3; i++ {
        num := <-c
        fmt.Println(num * num)
    }
}

func main() {
    fmt.Println("main() started")
    c := make(chan int, 3)
    var wg sync.WaitGroup
    
    wg.Add(1)   // <- increment the waitgroup counter
    go squares(c, &wg)

    c <- 1
    c <- 2
    c <- 3
    c <- 4
   
    wg.Wait() // <- wait for the counter to go back to 0
 
    fmt.Println("main() stopped")
}
  • in main() : close the channel when you are done feeding values,
    in squares : use range over the channel to get all the values to process
// you can tell the compiler "this channel will be only used as a receiver"
func squares(c <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()

    // consume all values until the channel is closed :
    for num := range c {
        fmt.Println(num * num)
    }
}

func main() {
   ...

   c <- 1
   c <- 2
   c <- 3
   c <- 4
   c <- 5
   c <- 6
   c <- 7
   close(c)  // close the channel to signal "no more values"

   wg.Wait()

   ...
}

With the above modifications, your program will always print all of its values on stdout before exiting.

playground : https://play.golang.org/p/qD_FHCpiub7