Search code examples
go

In a Goroutines with select case and default, once channel closed, default should not get executed


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\n", x)
            default:
            fmt.Printf("I didnt wait %d\n", x)
            }
            done.Done()
        }(i)
    }
    t0 := time.Now()
    close(finish) 
    fmt.Println("finish closed")
    done.Wait() 
    fmt.Printf("Waited %v for %d goroutines to stop\n",     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:

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.


Solution

  • 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\n", x)
                default:
                    fmt.Printf("I didnt wait %d\n", 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\n", time.Since(t0), n)
    }