2026-10-31·作者 Sijie Wang

gmp

GMP scheduling: Goroutine, Machine, Processor. M:N scheduling, work stealing, syscall handoff, netpoll, and who blocks whom.

GMP scheduling: Goroutine, Machine, Processor

The motivation: cheap tasks and real parallelism at the same time. An OS thread costs a large stack and kernel scheduling, so one thread per task cannot scale to a hundred thousand concurrent tasks. But tasks that only ever run on one thread cannot use many cores either. Go therefore runs M:N scheduling: many cheap user-level tasks (G) multiplexed onto a small number of OS threads (M), with a third piece (P) capping how many tasks truly run in parallel.

G = Goroutine. A cheap user-level task created by a go statement. It carries its own stack, which starts small and grows, and its own state. Creating one is cheap; that does not make it free (see the leak warning below).

M = Machine. An OS thread, the actual worker. An M runs Go code only while it holds a P. Without a P, an M runs nothing.

P = Processor. A scheduling context, not a CPU core. It holds a local run queue (capacity 256), a single priority slot called runnext, and scheduler bookkeeping such as a memory cache. The number of P is GOMAXPROCS, which defaults to the number of CPU cores. P is the token: holding a P is what admits an M to run Go code.

The scheduling loop, in order. An M with a P takes runnext first (a single slot that jumps the queue), then the head of its own local run queue, then a batch from the global queue, and finally steals from a randomly chosen victim P, taking about half of that P's local queue. An idle P actively takes work, so a long queue on one P next to an idle P does not stand.

A full local queue overflows in batches, it does not grow. When a local run queue is full at 256, half of it is moved to the global queue at once. There is no per-P expansion.

Blocking syscalls hand the P over. When an M enters a blocking system call, it releases its P (entersyscall), and an idle M, or a freshly created one, picks that P up and keeps running other goroutines. When the call returns, the M tries to take its old P back (the fast path); if no P is free, its goroutine waits on the global queue and the M parks. The P is never stuck behind one blocked thread.

Async preemption keeps one goroutine from starving the rest. The sysmon thread watches, and a goroutine that runs for about 10 ms without yielding is preempted (Go 1.14 and later, signal-based), so a tight loop cannot hold a P forever.

Network I/O does not block an M. Network waits go through netpoll (epoll/kqueue): the socket is handed to the poller, the goroutine parks waiting for an event, and the M goes on running other goroutines. This is the NIO shape, the same idea as Netty NIO.

Who blocks whom, the easily confused part. Three kinds of blocking, three different answers. Runtime suspension (channel send/receive, sleep, mutex): the goroutine waits, the M runs on, because the runtime parks the goroutine itself. Network wait: the goroutine waits on a netpoll event, the M runs on. Blocking syscall (file I/O, some cgo calls): the M is genuinely stuck in the kernel, so it hands its P over and another M runs other goroutines. Blocking in Go code is not the same as blocking a thread; only the third kind blocks an M, and even then it does not block the P.

Tuning knobs. GOMAXPROCS sets the P count; in a container it should follow the cgroup CPU quota (automaxprocs does this), not the host core count. The M count is demand-driven and capped at 10,000 (debug.SetMaxThreads); idle Ms spin briefly looking for work, then park and release their P.

GC and P. Stop-the-world pauses stop all P. Concurrent marking is done by GC-worker goroutines, and mutator goroutines help marking through GC assist, so marking work is shared rather than left to a dedicated crew.

An analogy, with two differences. GMP is like a connection pool whose members steal work: a limited set of execution slots is reused, and a task does not own a slot for its whole life. Difference one: a goroutine can be preempted and migrated across P and M, unlike a pooled connection held for a whole request. Difference two: work stealing means an idle P actively takes work, so there is no idle-P-next-to-long-queue situation.

Three pitfalls, corrected. One: P is not a CPU core; it is a scheduling context whose count is set by GOMAXPROCS. Two: M is not P; the M count can exceed P (blocked Ms hold no P) and can be fewer than P (idle Ps sit empty); the invariant is that Ms actually running Go code never exceed P. Three: not all blocking occupies an M; see the who-blocks-whom diagram above. Two more warnings: a full local queue overflows in batches instead of expanding, and goroutines are cheap, not free: a forgotten goroutine waiting forever still holds its stack and its references, which is a leak.

關於這一條

sijie 的一條 wiki 條目。本站的 AI 以同一套語料庫為依據、以 sijie 的口吻作答,並附有指向此類條目的引用——回答會花 sijie 的錢,所以它在存取碼後等待: 輸入存取碼 →

gmp — Sijie Wang