select chooses at random¶
Level: 201 · anyone who wrote the important case first and expected it to win
One line: When more than one case of a select can proceed, Go picks one of them at random, with every ready case equally likely, so the order of the cases is not a priority. A priority is something you write: a non-blocking check of the urgent channel, before the select that waits on everything.
Verified output of select_chooses_at_random_go.go — regenerated by tools/run_examples.py, never hand-typed.
1. Two cases, both ready on every one of 10000 selects
urgent chosen between 40% and 60% of the time: true
routine chosen between 40% and 60% of the time: true
2. 100 urgent and 100 routine messages already queued
plain select handled every urgent message first: false
urgent-first select handled every urgent message first: true
Reading the output¶
1. Writing a case first does not make it win. Both channels held a message before every one of the 10,000 selects, and the urgent case was written first. It was chosen between 40% and 60% of the time, and so was routine. The Go specification ↗ says that when one or more communications can proceed, a single one is chosen "via a uniform pseudo-random selection". The runtime does this by shuffling the cases into a new random order each time a select runs. In runtime/select.go ↗, that is the loop under the comment generate permuted order.
Why a threshold and not the counts. The counts change on every run:
urgent 5003 routine 4997
urgent 4999 routine 5001
urgent 4965 routine 5035
urgent 5065 routine 4935
urgent 4967 routine 5033
urgent 4989 routine 5011
urgent 4992 routine 5008
urgent 4978 routine 5022
urgent 4992 routine 5008
urgent 5036 routine 4964
If every select is a fair coin toss between the two cases, the number of urgent picks out of 10,000 has a mean of 5,000 and a standard deviation of 50. To fall outside 4,000–6,000, a count must land 20 standard deviations from the mean. The exact binomial probability of that is about 1.2 × 10−89, so the key cannot fail by chance on any machine where select really does choose uniformly. The key also catches the mistake it is about: a select that always took the first ready case would print false on both lines.
To see the counts on your own machine, run bash demo/tally.sh from this folder. It builds demo/tally_go.go and runs it ten times; give it a number for more runs.
2. Priority is something you write. Before either loop, 100 urgent and 100 routine messages were queued, and nothing added more. The plain select interleaved them, so at least one routine message came before the last urgent one. For the plain select to take every urgent message first by chance, it would have had to pick urgent 100 times in a row while both channels were ready, a probability of 2−100, about 7.9 × 10−31. urgentThenAny first looks at urgent alone, with a default so that looking does not wait. Only when urgent is empty does it go on to a select that waits on both. It handled all 100 urgent messages before any routine one.
What that priority does not promise. The inner select still chooses at random among the cases that are ready when it runs. Suppose it is waiting on both channels, and an urgent and a routine message arrive at almost the same moment: either can be taken first. Part 1 is that case. What the outer check does promise is that urgent work that was already waiting is never passed over for routine work. That is usually what a priority needs, for example a worker that checks whether it has been cancelled before it takes another job (Cancel reaches every goroutine).
select_chooses_at_random_go.go
select_chooses_at_random_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// When several cases of a select are ready, one is chosen at random: the order
// the cases are written in is not a priority. Priority is something you write.
//
// go run select_chooses_at_random_go.go
package main
import "fmt"
const selects = 10_000
func main() {
fmt.Println("1. Two cases, both ready on every one of", selects, "selects")
urgent := make(chan string, 1)
routine := make(chan string, 1)
fromUrgent, fromRoutine := 0, 0
for range selects {
if len(urgent) == 0 {
urgent <- "disk almost full"
}
if len(routine) == 0 {
routine <- "rotate the logs"
}
select {
case <-urgent: // written first
fromUrgent++
case <-routine:
fromRoutine++
}
}
fmt.Println(" urgent chosen between 40% and 60% of the time: ", between40and60(fromUrgent))
fmt.Println(" routine chosen between 40% and 60% of the time:", between40and60(fromRoutine))
fmt.Println()
fmt.Println("2. 100 urgent and 100 routine messages already queued")
urgentQueue, routineQueue := queued(100)
var plain []string
for range 200 {
plain = append(plain, plainSelect(urgentQueue, routineQueue))
}
fmt.Println(" plain select handled every urgent message first: ", urgentFirst(plain))
urgentQueue, routineQueue = queued(100)
var prioritized []string
for range 200 {
prioritized = append(prioritized, urgentThenAny(urgentQueue, routineQueue))
}
fmt.Println(" urgent-first select handled every urgent message first:", urgentFirst(prioritized))
}
// plainSelect takes whichever message the select picks.
func plainSelect(urgent, routine <-chan string) string {
select {
case <-urgent:
return "urgent"
case <-routine:
return "routine"
}
}
// urgentThenAny looks at urgent alone first, without waiting, and only when it
// is empty waits on both.
func urgentThenAny(urgent, routine <-chan string) string {
select {
case <-urgent:
return "urgent"
default:
select {
case <-urgent:
return "urgent"
case <-routine:
return "routine"
}
}
}
func between40and60(count int) bool {
return count >= selects*40/100 && count <= selects*60/100
}
// queued returns two buffered channels holding n messages each.
func queued(n int) (urgent, routine chan string) {
urgent = make(chan string, n)
routine = make(chan string, n)
for range n {
urgent <- "disk almost full"
routine <- "rotate the logs"
}
return urgent, routine
}
// urgentFirst reports whether no routine message came before the last urgent one.
func urgentFirst(handled []string) bool {
seenRoutine := false
for _, kind := range handled {
if kind == "routine" {
seenRoutine = true
} else if seenRoutine {
return false
}
}
return true
}
What to do¶
- Never rely on the order of cases. Write them in whatever order reads best, because the runtime ignores that order.
- For a priority, check the urgent channel first with a non-blocking
select, then wait on everything. The same shape puts<-ctx.Done()first in a worker loop, so a cancelled worker does not start one more job. - Count on the fairness. Because the choice is random, a channel that is always ready cannot starve another channel that is also ready. Part 1 shows each of the two getting about half of the selects.
- Test a random choice with a threshold so wide that missing it by chance is out of the question, never with the counts themselves.
In other languages¶
- The Rust library's Channels ↗ page uses
std::sync::mpsc, which has noselectat all. The crates that add one agree with Go about fairness.crossbeam_channel::select!↗ picks a random operation when several are ready.tokio::select!↗ picks a random branch to check first by default. Unlike Go, it also has abiased;mode, which checks branches from top to bottom and leaves fairness to you. - The Concurrency library's chapter 05, Message passing, which covers channels and
selectacross languages, is planned.
Sources¶
- The Go specification, Select statements ↗.
runtime/select.go↗ in Go 1.25.5, whereselectgobuilds its random polling order.- Katherine Cox-Buday, Concurrency in Go (O'Reilly, 2017), chapter 3, "Go's Concurrency Building Blocks", the section on the
selectstatement, which counts the choices between two ready channels. - Burak Serdar, Effective Concurrency in Go (Packt, 2023), chapter 2, "Go Concurrency Primitives", the section on channels.