I am trying to understand the behaviour of closed channel in a select block with default case, but got confused with this following output. Here am invoking 50 goroutines and closing the finish channel.
func testClosedChannelBehavior() {
const n = 50
finish := make(chan bool)
var done sync.WaitGroup
for i := 0; i < n; i++ {
done.Add(1)
go func(x int) {
select {
case <-time.After(1 * time.Hour):
case <-finish:
fmt.Printf("received finish %d
", x)
default:
fmt.Printf("I didnt wait %d
", x)
}
done.Done()
}(i)
}
t0 := time.Now()
close(finish)
fmt.Println("finish closed")
done.Wait()
fmt.Printf("Waited %v for %d goroutines to stop
", time.Since(t0), n)
}
I expected once any goroutine prints "received finish", default case should not get executed by any other goroutines i.e. "I didnt wait" should not get printed. But the output is not consistent. At times it behaves as expected, but on running multiple times, I could see unexpected output as below: =====output====== I didnt wait 0 received finish 7 finish closed received finish 13 received finish 10 received finish 32 received finish 5 received finish 14 received finish 33 received finish 42 received finish 11 received finish 4 received finish 23 received finish 44 received finish 49 received finish 15 received finish 24 received finish 31 received finish 16 received finish 40 received finish 41 received finish 6 received finish 26 I didnt wait 1 received finish 19 received finish 8 received finish 43 received finish 29 received finish 20 received finish 46 received finish 12 received finish 36 received finish 47 received finish 37 received finish 35 received finish 30 received finish 39 received finish 22 received finish 28 I didnt wait 2 received finish 17 received finish 45 I didnt wait 9 received finish 48 received finish 34 I didnt wait 3 received finish 25 received finish 38 received finish 27 received finish 18 received finish 21 Waited 394.999µs for 50 goroutines to stop
I was going through this link expecting close(finish) would signal others, those still waiting, to behave alike.
Calls to fmt.Printf involve a syscall. Syscalls automatically cause that goroutine to be rescheduled, as it has to wait on the OS to finish that syscall. That means it's very possible for some of those goroutines to run the select statement and select the default case, but not print to console yet.
Edit: Also, if you're running this on a system with more than one thread, the go runtime by default will run several go routines in parallel (matching the number of OS threads), meaning some of those goroutines could be executing at the same time as the channel close and reach the select statement before the channel close occurs in the main goroutine.
If you add in a sync channel to ensure that the channel close operation occurs before the select
happens in any of the goroutines, it works as expected:
https://play.golang.org/p/XtUYaihKgRT
func testClosedChannelBehavior() {
const n = 50
finish := make(chan bool)
proceed := make(chan struct{})
var done sync.WaitGroup
for i := 0; i < n; i++ {
done.Add(1)
go func(x int) {
<-proceed
select {
case <-time.After(1 * time.Hour):
case <-finish:
fmt.Printf("received finish %d
", x)
default:
fmt.Printf("I didnt wait %d
", x)
}
done.Done()
}(i)
}
t0 := time.Now()
close(finish)
fmt.Println("finish closed")
close(proceed)
done.Wait()
fmt.Printf("Waited %v for %d goroutines to stop
", time.Since(t0), n)
}