Go scheduler: GMP model
- Last updated
- Prerequisites:
- Go goroutines and channels basics
- Understanding of OS threads vs user-space scheduling
- go
- goroutine
- scheduler
- gmp
- concurrency
- runtime
Read at your depth
The practical view
go func() { ... }() starts a goroutine. Goroutines are lightweight user-space threads: they start with a tiny stack (2KB, grows) and are multiplexed by the Go runtime onto a small set of OS threads. The runtime scheduler uses the GMP model: G is a goroutine, M is an OS thread (machine), P is a processor (logical CPU, default GOMAXPROCS = number of cores). A goroutine blocks on a channel/syscall; the runtime parks its M or the G and schedules another G. You do not manage threads; the runtime balances work across Ps.
Legacy vs modern
Blocking the main thread vs goroutine-parallel work
Serialized sequential work leaves cores idle; distributing across goroutines uses GOMAXPROCS workers through the GMP scheduler.
var total int64
for _, n := range nums {
total += heavy(n) // one core does everything
}jobs := make(chan int); results := make(chan int64)
// N workers consume jobs; the GMP scheduler keeps all Ps busyInterview gotchas
Context
This checks whether the candidate understands cooperative vs preemptive scheduling in Go — a frequent senior-level probing point.
The mechanical answer
Before Go 1.14, the scheduler was mostly cooperative: a G yielded only at safe points — function calls, channel operations, allocation, etc. A tight CPU loop with no function calls (like for {}) never hit a safe point, so it could starve other goroutines indefinitely on its P. Go 1.14 added asynchronous preemption: the runtime sends a SIGURG signal to the thread running the misbehaving G, which interrupts the loop at a safe point inserted by the compiler and allows the scheduler to run another G. Stack-scan safe points and the preemption flag in the G struct coordinate the interrupt. Preemption is still not fully arbitrary — some C code and certain runtime internals are non-preemptible.
Trap
The trap answer is 'goroutines are preempted by the OS like threads'. Goroutines are user-space; OS-level preemption does not apply. Another trap: 'tight loops never yield, so they hang forever' — that is true pre-1.14 and demonstrably wrong now, but the nuance (async preemption via signals, safe points, non-preemptible regions) is what the interviewer wants. Mentioning runtime.Gosched() as the cooperative alternative shows you know both mechanisms.
Context
This probes the GMP mechanics under blocking I/O — the interviewer wants the detach/reacquire dance, not just 'it blocks'.
The mechanical answer
When a G makes a blocking syscall, the runtime calls entersyscall: the G is marked as in syscall, and the P is released from the M. If other work is pending, another M can be woken to pick up the released P and its run queue, so the CPU stays busy. When the syscall returns, exitsyscall tries to reacquire the original P; if it is taken, the G is placed into the global run queue and the M either finds another P or idles/parks. Network I/O is special: the netpoller parks the G (not the M) on the network poller, so sockets never trigger the M detach path. This is why blocking syscalls cost an M (thread), but channel/socket waits cost only a G.
Trap
The naive answer is 'the goroutine blocks and everything stops'. Only the M detaches; the P and other Gs continue. Another trap: 'network I/O detaches the M like syscalls' — socket waits use the netpoller and park the G instead, which is why HTTP-heavy servers stay cheap. Candidates who distinguish syscall-blocking (M detach) from netpoller-blocking (G park) demonstrate real runtime understanding.