How does work stealing work, and in what order does a P look for work?
Question 150HardGo 1.22 to 1.25
When the current goroutine blocks or gives up the CPU, the P calls schedule() → findRunnable(). It checks these sources, roughly in order:
- Every 61st scheduling tick, the global run queue first, so that goroutines there are not starved.
- The
runnextslot, then the local run queue, a lock-free ring of 256 slots. - The global run queue, taking a batch of goroutines.
- A non-blocking netpoll, which picks up goroutines whose I/O is ready.
- Stealing: it visits other Ps in random order and takes half of a victim's local queue. It can also steal the victim's timers and, as a last resort, its
runnext. - Only then does the M release its P, park, and possibly block in netpoll.
If a local queue overflows, half of it moves to the global queue. Spinning Ms are threads that are looking for work. The runtime limits them to about GOMAXPROCS/2 so they do not waste CPU, but keeps at least one spinning when there is new work, so runnable goroutines get picked up quickly.
Gotcha: goroutines do not run in FIFO order. The runnext slot gives LIFO behaviour for the most recently created goroutine.
More on Goroutines & the Scheduler
- Q148How is a goroutine different from an OS thread?
- Q149Explain the GMP model of the Go scheduler.
- Q151What does this print with GOMAXPROCS(1), and why?
- Q152What is preemption in Go? Explain cooperative vs. asynchronous preemption (Go 1.14).
- Q153What does this program do on Go 1.13 vs Go 1.14+?
- Q154How does a goroutine's stack grow? What changed from segmented to contiguous stacks?