TRT
Sign in
easyExpertconcept

Explain the Go GMP scheduler in detail—what are G, M, and P, what is the `runnext` slot, how does work-stealing work, how does the Netpoller differ from.

Walk through `G`, `M`, `P`, the 1-slot LIFO `runnext` optimization, `schedtick % 61` global fairness, 50% work-stealing, `epoll` Netpoller vs `handoffp`, and Kubernetes CFS quota throttling.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

The Go scheduler (runtime/proc.go) multiplexes $N$ Goroutines (G, ~2 KB stack + gobuf PC/SP) onto $M$ OS Threads (M) using exactly GOMAXPROCS Logical Processors (P). An OS thread M must hold a P to execute Go code! Each P owns a lock-free local runqueue ([256]*g) plus a 1-slot runnext priority slot (and all Ps share a mutex-guarded global runqueue checked every 61 ticks so global tasks never starve). Look at how GMP handles three critical runtime events: (1) Work-Stealing (findRunnable): When P0's local runqueue is empty, it checks the global queue/netpoller, and then randomly picks another processor P1 and steals half of P1's local runqueue in one atomic batch! (2) Network I/O (netpoller): When G1 reads from a non-blocking socket, G1 is parked in the Netpoller (epoll/kqueue/iocp) and M0 keeps P0 and immediately runs the next G2 from P0's runqueue with zero OS thread switches! (3) Blocking OS Syscall / CGO (exitsyscall): When G1 makes a blocking disk syscall or C call, M0 detaches from P0 (handoffp), allowing another idle thread M1 to pick up P0 and keep running other goroutines while M0 blocks in the kernel!

At any instant, at most GOMAXPROCS OS threads (M) are actively executing Go user code (each bound to one P), while any number of additional M threads may be blocked inside OS syscalls or CGO calls detached from a P. Because executing Go instructions requires acquiring one of the GOMAXPROCS P tokens and exitsyscall detaches P from the blocking M before the kernel blocks M, blocking syscalls cannot starve runnable goroutines on P while active user-space Go execution stays strictly bounded to GOMAXPROCS cores.

Switching from G1 to G2 on the same M + P when a socket blocks on the Netpoller saves/restores only a few CPU registers (PC, SP) in ~150–200 nanoseconds—roughly 10x faster than a Linux kernel thread context switch (~1.5–3 microseconds)! In production systems (Go runtime/proc.go (schedule, findRunnable, stealWork, entersyscall), runtime/netpoll_epoll.go, and go.uber.org/automaxprocs in every production Kubernetes Go deployment.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how the Go M:P:N Scheduler (G, M, P), Work-Stealing, and Netpoller / Syscall Handoff operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Goroutine Switch on Netpoller, Blocking Syscall / cgo Call) with concrete numbers.
  • Identifies real-world failure modes (Deploying Go on a 64-core Kubernetes node with a 2-CPU container limit and default GOMAXPROCS=64) and explains how production systems mitigate them.

Common mistakes

  • Treating the Go M:P:N Scheduler (G, M, P), Work-Stealing, and Netpoller / Syscall Handoff as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Making 1,000,000 synchronous blocking disk reads or cgo calls concurrently without bounding concurrency).
  • Forgetting boundary conditions and error recovery (Calling runtime.LockOSThread() inside a goroutine that later exits without calling UnlockOSThread()).

This answer walks

Practice follow-ups

  1. 01What is the 1-slot `P.runnext` field on a Logical Processor, and why does putting a newly woken goroutine (e.g., after `ch <- v`) into `runnext` improve CPU cache locality?
  2. 02Why does calling C code via `cgo` cost ~40–60x more overhead than a normal Go function call? (Must switch from G's 2 KB growable stack to M's 8 MB system stack, save scheduler state, and notify.

Not started

Sign in to save your learning progress.

Sign in to save