28. The scheduler deep dive: GMP and work-stealing
28.1 The three letters: G, M, P
- G (Goroutine): your lightweight function-in-flight, with its own small growable stack.
- M (Machine): an OS thread — the thing the operating system actually schedules.
- P (Processor): a scheduling context, one per
GOMAXPROCS, each holding a local run queue of Gs ready to execute. An M must hold a P to run Go code.
28.2 Why this design
GOMAXPROCS (default: runtime.NumCPU()) caps how many Gs can execute Go
code simultaneously, while thousands of Gs can be parked waiting on
channels, I/O, or locks. When an M would block on a syscall, the runtime
detaches it from its P and hands that P to another M, so blocking one
goroutine on I/O never blocks the other goroutines scheduled on the same
logical processor.
28.3 Work stealing
Each P has a local run queue; when a P's local queue empties, it steals
half the Gs from another P's queue rather than going idle. This keeps
all GOMAXPROCS processors busy under uneven workloads without a global
lock on every scheduling decision.
28.4 Cooperative preemption and why it (mostly) doesn't matter to you
Since Go 1.14, the scheduler can preempt a long-running goroutine even
without a function call or channel operation providing a natural
checkpoint (async preemption via signals) — meaning a tight
for {} CPU-bound loop no longer starves other goroutines on the same P
the way it could in much older Go versions.
28.5 Real-world example
Setting GOMAXPROCS explicitly used to matter a lot in containers:
older Go releases defaulted mostly from the host CPU count / affinity, not
the container's CPU quota, so a container capped at 2 CPUs but running on a
64-core host could create too much runnable parallelism and hit CPU
throttling. Go 1.25 added cgroup-aware default GOMAXPROCS behavior on
Linux (GODEBUG=containermaxprocs=... controls it), and
go.uber.org/automaxprocs covers older deployments — still worth checking
explicitly any time you deploy Go into Kubernetes.
Applied in the series: Linux guide, Chapter 75: container-aware GOMAXPROCS (Go 1.25+) measured under cgroup CPU limits, including 13.7 s of throttling when GOMAXPROCS was forced too high.