How does select work when multiple channels are involved?
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?
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()
}()