Why is receiving from a channel ordered in Golang?

76 Views Asked by At

Here is an example:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)
    for i := 0; i < 5; i++ {
        time.Sleep(time.Millisecond)
        go func(i int) {
            for {
                n := <-ch
                fmt.Printf("Goroutine %d got %d\n", i, n)
            }
        }(i)
    }
    for i := 0; i < 20; i++ {
        time.Sleep(time.Millisecond)
        ch <- i
    }
    time.Sleep(time.Millisecond)
}

After the first loop, we have 5 goroutines that are waiting for messages from the channel. Then, we have the second loop. My expectations were that messages would be delivered to the random goroutines. Unfortunately, the output is always the same:

Goroutine 0 got 0
Goroutine 1 got 1
Goroutine 2 got 2
Goroutine 3 got 3
Goroutine 4 got 4
Goroutine 0 got 5
Goroutine 1 got 6
Goroutine 2 got 7
Goroutine 3 got 8
Goroutine 4 got 9
Goroutine 0 got 10
Goroutine 1 got 11
Goroutine 2 got 12
Goroutine 3 got 13
Goroutine 4 got 14
Goroutine 0 got 15
Goroutine 1 got 16
Goroutine 2 got 17
Goroutine 3 got 18
Goroutine 4 got 19

The delivery order, in this case, is first-come-first-got. Is it documented?

How can I change the behavior and deliver each message to a random goroutine?

1

There are 1 best solutions below

8
Schwern On

The delivery order, in this case, is first-come-first-got. Is it documented?

Yes, in The Go Programming Language Specification.

Channels act as first-in-first-out queues. For example, if one goroutine sends values on a channel and a second goroutine receives them, the values are received in the order sent.

How can I change the behavior and deliver each message to a random goroutine?

AFAIK there is no way to alter the channel implementation. Simplest option would be to randomize your channel input.

However, the apparent neat order is only there because you're sleeping. Sleep in concurrent code is generally a bad sign. There's no way to predict how long each operation will take, so you wind up with either conservatively long sleeps leading to inefficient code, or sleeps that are sometimes too short leading to hard to debug errors.

There's no need to sleep when working with channels. If you try to send to a channel which has no room, the send will block. If you try to receive from a channel with nothing in it, the receive will block.

If you remove the sleeps and let Go operate concurrently, the nice neat order will go away. Instead, use channel buffering and sync.WaitGroup to coordinate.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    // Note that production code would add a buffer here.
    // Without a buffer, workers will wait for input.
    // A buffer at least equal to the number of workers will run
    // more efficiently.
    ch := make(chan int)

    for i := 0; i < 5; i++ {
        // Add the new worker to the wait group. Basically just increments a counter.
        wg.Add(1)

        go func(i int) {
            // Tell the wait group this worker is done. Basically decrements a counter.
            // defer ensures this will happen no matter how the function exits, even if it errors.
            defer wg.Done()

            // Using range(ch) lets the loop exit once the channel is closed
            for n := range(ch) {
                fmt.Printf("Goroutine %d got %d\n", i, n)
            }
        }(i)
    }
    
    // Because ch has no buffer, this will wait until a worker reads from
    // the channel before adding another value.
    // This is causing a bottleneck as workers wait for input.
    for i := 0; i < 20; i++ {
        ch <- i
    }

    // Close the channel so workers know there is no more input coming.
    close(ch)
    
    // main() will not wait for goroutines to finish.
    // This will wait until all workers call wg.Done().
    wg.Wait()
}

Demonstration.

Now that we're allowing everything to operate concurrently, the order of processing will not exactly be random, but also won't be quite so ordered as each goroutine reads from the channel as fast as they can. Here is one possible output, it will vary from run to run.

Goroutine 4 got 0
Goroutine 4 got 5
Goroutine 3 got 2
Goroutine 3 got 7
Goroutine 3 got 8
Goroutine 3 got 9
Goroutine 0 got 1
Goroutine 0 got 11
Goroutine 2 got 4
Goroutine 2 got 13
Goroutine 2 got 14
Goroutine 2 got 15
Goroutine 2 got 16
Goroutine 1 got 3
Goroutine 1 got 18
Goroutine 4 got 6
Goroutine 2 got 17
Goroutine 0 got 12
Goroutine 3 got 10
Goroutine 1 got 19

In this case the first 5 reads from the channel were...

  1. Goroutine 4
  2. Goroutine 0
  3. Goroutine 3
  4. Goroutine 1
  5. Goroutine 2

Then 4 finished its loop first and read 5 from the channel. And so on.


To better understand the code above, I recommend reading Go By Example starting at Goroutines and continuing on until WaitGroups.