Introduction
The Go runtime includes its own scheduler that maps goroutines to OS threads. Understanding the GMP model helps you reason about performance, diagnose scheduling issues, and make informed decisions about GOMAXPROCS.
Key Concepts
- G (Goroutine): A goroutine with its stack, instruction pointer, and state.
- M (Machine): An OS thread that executes goroutines.
- P (Processor): A logical processor holding a local run queue. The number of Ps equals
GOMAXPROCS. - Work Stealing: When a P's queue is empty, it steals goroutines from other Ps to balance load.
Real World Context
If you deploy a Go service on a 4-core machine, the default GOMAXPROCS of 4 creates 4 Ps. In a container with a CPU limit of 2 cores, GOMAXPROCS still defaults to the host CPU count unless you use the automaxprocs library or set it manually, potentially wasting CPU cycles spinning up threads that get throttled.
Deep Dive
The Go scheduler uses three entities:
- Each P has a local run queue of goroutines ready to execute.
- An M attaches to a P to execute goroutines from its queue.
- When a goroutine blocks (I/O, channel, syscall), the M detaches from its P and parks. Another M takes over the P to keep running other goroutines.
- If a P's local queue is empty, it steals half the goroutines from another P's queue.
GOMAXPROCS controls the number of Ps (the parallelism level):
goruntime.GOMAXPROCS(4) // Use 4 logical processors n := runtime.GOMAXPROCS(0) // Query current value without changing it
The default value equals the number of CPU cores detected by the runtime.
Common Pitfalls
- Setting GOMAXPROCS too high in containers — In Docker/Kubernetes, the Go runtime sees the host CPU count, not the container limit. Use
go.uber.org/automaxprocsto respect cgroup limits. - Assuming more Ps always means faster — For I/O-bound workloads, increasing GOMAXPROCS beyond CPU count provides no benefit and adds overhead.
Best Practices
- Leave GOMAXPROCS at default for most applications — The runtime chooses a sensible default. Only override it when profiling shows a benefit.
- Use
runtime.NumGoroutine()for monitoring — Track active goroutine count in metrics to detect leaks early.
Summary
- G, M, P: goroutine, OS thread, logical processor with a run queue.
- GOMAXPROCS sets the number of Ps (defaults to CPU count).
- Work stealing keeps all Ps busy by redistributing goroutines.
- In containers, set GOMAXPROCS explicitly or use
automaxprocs.
Code Examples
go
import "runtime"
func main() {
fmt.Println("CPUs:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
fmt.Println("Goroutines:", runtime.NumGoroutine())
}