Suppose that the following situation occurs:
We have the Consumer
function below, running in a goroutine.
Another goroutine is sending integers on the intChan
channel without any delay. In other words, on every iteration of the for-loop, there is a value ready to be received on the intChan
.
The goroutine that started the Consumer
goroutine, has cancelled the context passed into the Consumer
. Hence, the ctx.Done()
channel also has a value ready to be received.
Question:
select
will pick one case randomly, since both are ready to run.select
won't keep picking the <- intChan
case? How do we know that the <- ctx.Done()
case will eventually be selected, if both cases are ready in every iteration of the for-loop?func Consumer(ctx context.Context, intChan chan int) {
for {
select {
case <-ctx.Done():
return
case i := <-intChan:
foo(i)
}
}
}
I've tried using the Consumer
function, in the program below.
Both the Consumer
and Producer
goroutines always seem to terminate, in several runs of this program.
Why don't we end up with runs where the <-ctx.Done()
case is never executed?
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
ctx, cancelFunc := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(2) // add 2, because we spawn 2 goroutines
Producer(ctx, &wg)
fmt.Println(time.Now())
time.Sleep(time.Second * 5) // cancel the context after 5 seconds
cancelFunc()
fmt.Println("CANCELLED")
wg.Wait() // wait till both producer and consumer goroutines terminate
fmt.Println(time.Now())
}
func Producer(ctx context.Context, wg *sync.WaitGroup) {
intChan := make(chan int)
go Consumer(ctx, intChan, wg)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case intChan <- 1:
}
}
}()
}
func Consumer(ctx context.Context, intChan chan int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case _ = <-intChan:
}
}
}
There is no guarantee. The simplest way to guarantee termination would be to check for error with ctx.Err()
outside the select statement. It is also common to return the error back to the code that passed the context. I would write the Consumer func like this:
func Consumer(ctx context.Context, intChan chan int) error {
for ctx.Err() == nil {
select {
case <-ctx.Done():
case i := <-intChan:
foo(i)
}
}
return ctx.Err()
}