1

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?

  • Yes, you will not get deadlock as queue in channel is consumed. On the other hand, if you consume no elements or only one you will get deadlock. This happens because elements will stay in channel forever, without anything consuming them. – Jakub Dóka Jul 19 '21 at 09:15
  • Thank you for your answer. Just a little correction. If we consume no elements then the deadlock occurs otherwise it will work fine. – Rabia Iftikhar Jul 20 '21 at 13:35

2 Answers2

4

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.

cod3rboy
  • 717
  • 7
  • 9
  • Thank you for your clarification. It's clear to me now but this is the case only in buffered channels, not the unbuffered channels. – Rabia Iftikhar Jul 20 '21 at 13:46
  • For unbuffered channels, each send operation blocks until the value is received. And each receive operation blocks until a value is sent to the channel. – cod3rboy Nov 07 '22 at 09:40
0

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

LeGEC
  • 46,477
  • 5
  • 57
  • 104
  • The order of execution is not the problem because values are passed using channels and channels are FIFO. – cod3rboy Jul 19 '21 at 09:31
  • @cod3rboy : it's not a *problem*, it's just that OP statements such as "(3 values example) the main goroutine will not block and the program exists" or "(4 values example) main goroutine blocks and squares goroutine drains out all the values" are not a description of all the cases that can happen. In its current form, the OP's code gives very little certainty to what will happen. – LeGEC Jul 19 '21 at 09:36
  • Can you specifically point which code statement/block in OP program is creating uncertainty of execution order? – cod3rboy Jul 19 '21 at 09:46
  • 2
    the last intruction of the `main()` goroutine is "stop here and exit the program". This instruction can happen earlier or later, depending on when the scheduler decides to execute it. So : the first OP example may well print `1 4 9` before exiting (there is no obligation for the scheduler to exit *before* it executes the `square()` goroutine), the last example may exit right after `1`, before executing `4 9 16 25`. – LeGEC Jul 19 '21 at 11:35
  • The values are placed sequentially in the channel, so the OP will always see `1` before `4` before `9` before ... ; the channel has a capacity of `3` so the runtime is forced to yield at least once to the `square()` goroutine (and execute the instruction `num := <-c`) when the `main()` goroutine tries to send a value over the channel when it is full. That part is certain. Other than that, many interleavings of `square()` and `main()` may happen. – LeGEC Jul 19 '21 at 11:56