How does select work when multiple channels are involved?

17,964

Solution 1

The Go select statement is not biased toward any (ready) cases. Quoting from the spec:

If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.

If multiple communications can proceed, one is selected randomly. This is not a perfect random distribution, and the spec does not guarantee that, but it's random.

What you experience is the result of the Go Playground having GOMAXPROCS=1 (which you can verify here) and the goroutine scheduler not being preemptive. What this means is that by default goroutines are not executed parallel. A goroutine is put in park if a blocking operation is encountered (e.g. reading from the network, or attempting to receive from or send on a channel that is blocking), and another one ready to run continues.

And since there is no blocking operation in your code, goroutines may not be put in park and it may be only one of your "producer" goroutines will run, and the other may not get scheduled (ever).

Running your code on my local computer where GOMAXPROCS=4, I have very "realistic" results. Running it a few times, the output:

finish one acount, bcount 1000 901
finish one acount, bcount 1000 335
finish one acount, bcount 1000 872
finish one acount, bcount 427 1000

If you need to prioritize a single case, check out this answer: Force priority of go select statement

The default behavior of select does not guarantee equal priority, but on average it will be close to it. If you need guaranteed equal priority, then you should not use select, but you could do a sequence of 2 non-blocking receive from the 2 channels, which could look something like this:

for {
    select {
    case <-chana:
        acount++
    default:
    }
    select {
    case <-chanb:
        bcount++
    default:
    }
    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

The above 2 non-blocking receive will drain the 2 channels at equal speed (with equal priority) if both supply values, and if one does not, then the other is constantly received from without getting delayed or blocked.

One thing to note about this is that if none of the channels provide any values to receive, this will be basically a "busy" loop and hence consume computational power. To avoid this, we may detect that none of the channels were ready, and then use a select statement with both of the receives, which then will block until one of them is ready to receive from, not wasting any CPU resources:

for {
    received := 0
    select {
    case <-chana:
        acount++
        received++
    default:
    }
    select {
    case <-chanb:
        bcount++
        received++
    default:
    }

    if received == 0 {
        select {
        case <-chana:
            acount++
        case <-chanb:
            bcount++
        }
    }

    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

For more details about goroutine scheduling, see these questions:

Number of threads used by Go runtime

Goroutines 8kb and windows OS thread 1 mb

Why does it not create many threads when many goroutines are blocked in writing file in golang?

Solution 2

As mentioned in the comment, if you want to ensure balance, you can just forgo using select altogether in the reading goroutine and rely on the synchronisation provided by unbuffered channels:

go func() {
    for {
        <-chana
        acount++
        <-chanb
        bcount++

        if acount == 1000 || bcount == 1000 {
            fmt.Println("finish one acount, bcount", acount, bcount)
            break
        }
    }
    wg.Done()
}()
Share:
17,964

Related videos on Youtube

Terry Pang
Author by

Terry Pang

Updated on June 04, 2022

Comments

  • Terry Pang
    Terry Pang almost 2 years

    I found when using select on multiple non buffered channels like

    select {
    case <- chana:
    case <- chanb:
    }
    

    Even when both channels have data, but when processing this select, the call that falls in case chana and case chanb is not balanced.

    package main
    
    import (
        "fmt"
        _ "net/http/pprof"
        "sync"
        "time"
    )
    
    func main() {
        chana := make(chan int)
        chanb := make(chan int)
    
        go func() {
            for i := 0; i < 1000; i++ {
                chana <- 100 * i
            }
        }()
    
        go func() {
            for i := 0; i < 1000; i++ {
                chanb <- i
            }
        }()
    
        time.Sleep(time.Microsecond * 300)
    
        acount := 0
        bcount := 0
        wg := sync.WaitGroup{}
        wg.Add(1)
        go func() {
            for {
                select {
                case <-chana:
                    acount++
                case <-chanb:
                    bcount++
                }
                if acount == 1000 || bcount == 1000 {
                    fmt.Println("finish one acount, bcount", acount, bcount)
                    break
                }
            }
            wg.Done()
        }()
    
        wg.Wait()
    }
    

    Run this demo, when one of the chana,chanb finished read/write, the other may remain 999-1 left.

    Is there any method to ensure the balance?

    found related topic
    golang-channels-select-statement

  • icza
    icza over 6 years
    Virtual machines often have 1 VCPU assigned which results in GOMAXPROCS defaulting to 1, which would explain the "one-sided" effect you experienced. You can verify it with the fmt.Println(runtime.GOMAXPROCS(0)) code.
  • Ravi R
    Ravi R over 6 years
    Ok. I tried with above code. Actually, I have 2 VCPU's allocated. It does look one-sided still on my system. These are the numbers for acount, bcount : 19 1000, 243 1000, 1000 1, 1000 38, 1000 635, 262 1000, 1 1000, 1000 51, 1 1000, 1000 98, 1 1000, 245 1000, 1 1000
  • icza
    icza over 6 years
    Since there are 3 goroutines working in the example (2 producers and 1 consumer), as long as available CPUs are less than 3, the side effect can be explained with the same reasoning. In my example I had 4 CPUs, so this was not experienced.
  • aiguy
    aiguy over 5 years
    wouldnt your example with equal priority waste more CPU time doing empty iterations of for{} loop with more sparsity in data going through the channels? So basically one goroutine would always use one CPU to 1) check chana - empty 2) select default 3) check chanb - empty 4) select default 5) evaluate if - false. Over and over instead of sharing that CPU with other goroutines to do useful work?
  • icza
    icza over 5 years
    @aiguy Nice catch! You're right, thanks for pointing that out. I added a solution for that case as well. See edited answer.
  • Mạnh Quyết Nguyễn
    Mạnh Quyết Nguyễn almost 5 years
    @icza if I select from 2 channels ch1, ch2 and both has elements to read at the same time. The go routine will pick one case to execute, say ch2. My question is: Will the available element from ch1 be pulled out or not?
  • icza
    icza almost 5 years
    @MạnhQuyếtNguyễn No, it won't be, only the chosen communication op is executed.