skip to content

Goroutine Scheduler (GMP)

How goroutines actually get onto CPUs: the G-M-P model, the run queues and work stealing that feed it, preemption of goroutines that will not yield, and what happens when one blocks on a syscall or the network. Backend interviews reach for this the moment you claim goroutines are cheap.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

page 1 of 2

In Go's goroutine scheduler, what do G, M and P stand for, and what does each represent?

level: juniorimportance: must knowfreq 58%

answer

  1. three letters, three different objects
  2. one belongs to the kernel, two do not
  3. a thread needs a permit to run Go
  4. the permit count is GOMAXPROCS

basics

~20 s

G is a goroutine, M is an OS thread, and P is a scheduling context an M must hold to run Go code. GOMAXPROCS sets how many Ps exist, so it caps how much Go code runs at once.

solid answer

~50 s

G, M and P are the three runtime structures Go's scheduler juggles. A **G** is a goroutine: its stack, its instruction pointer and its scheduling state. An **M** is a machine, meaning a real OS thread — the only one of the three the kernel knows about. A **P** is a processor, a scheduling context that carries the resources needed to run Go code: a run queue of ready goroutines and a per-P memory cache. Execution is always a triple: an M picks up a P, the P hands it a G, and the M runs that G's code. There are usually far more Gs than Ms, and the number of Ps is fixed by `GOMAXPROCS`, so `GOMAXPROCS` is the ceiling on how many goroutines execute Go code simultaneously — that is Go's M:N multiplexing.

go deeper

for a junior

Be ready to name all three letters without hesitating and say which one the operating system knows about. Saying "P is a CPU core" is the answer that ends this question badly.

for a middle

An interviewer expects you to describe execution as an M+P+G triple and to explain that goroutine switching happens in user space because of it. Know that the P count comes from GOMAXPROCS.

for a senior

Show that you can read runtime output through this vocabulary — one stack per G in a dump, Ms in a host thread count, Ps in scheduler output — and that you know the three counts move independently in production.

for a principal

Own the framing that the P count is a capacity decision your platform makes on every service, not a detail: it is the number of Go-code execution slots each container gets, and it defaults from the environment you hand the process.

## The three structures Go's scheduler is built from three runtime types, named by single letters in the runtime source and in every conference talk about it. **G — goroutine.** A `g` is the runtime's record of one goroutine: its stack (small at first, a couple of kilobytes, and grown by the runtime when needed), the saved registers and program counter it resumes from, its status (runnable, running, waiting, dead), and bookkeeping such as which M is currently running it. A `go f()` statement allocates or reuses a `g` and marks it runnable. It does **not** ask the operating system for anything — this is why a program can hold hundreds of thousands of goroutines, where the same number of OS threads would be impossible. **M — machine.** An `m` is an actual OS thread. This is the only one of the three the kernel schedules, sees in a thread listing, or delivers a signal to. An M executes machine code; on its own it has no idea which goroutine should run next. **P — processor.** A `p` is a *scheduling context*, and it is the piece people find surprising because it corresponds to nothing in the operating system. A P is a permit plus a working set: it owns a queue of goroutines that are ready to run, and it owns per-P caches (most notably a memory-allocation cache) so that common operations need no global lock. **An M must be holding a P to execute Go code.** The number of Ps is created at startup from `GOMAXPROCS` and does not drift on its own. ## Why the triple Running code always means a triple: **M + P + G**. The thread supplies execution, the P supplies the right to run Go code and the state to decide what runs, and the G supplies the actual work. When a goroutine stops being runnable — it blocks on a channel, sleeps, or finishes — the M does not stop; it takes the next G from the P it holds and continues. Goroutine switching therefore happens entirely in user space, with no kernel transition, which is what makes it cheap. The P also explains a boundary that confuses newcomers. If parallelism were bounded by the *thread count*, then every thread that stopped being useful for Go code would eat a slot in that budget. Separating the permit (P) from the worker (M) lets the runtime keep exactly `GOMAXPROCS` Go-code slots while the thread count moves independently. ## M:N multiplexing Go is an M:N runtime: many goroutines are multiplexed onto a smaller number of OS threads. Concretely: - **Gs** — as many as your program creates; thousands or millions is normal. - **Ms** — as many as the runtime finds it needs; not fixed by you and not equal to the number of Ps. - **Ps** — exactly `GOMAXPROCS`, and that number is what bounds simultaneous execution of Go code. So "how many goroutines can run at the same time?" has two answers depending on what you mean. *Concurrently* — in flight, interleaved — as many as you create. *Simultaneously*, on different CPUs at the same instant — at most the number of Ps. ## Where the letters show up Once you know the vocabulary, the runtime's own output becomes readable: scheduler debug output talks about Ps and their queues, a panic dump prints one stack per G, and thread-level tooling on the host counts Ms. `runtime.GOMAXPROCS(0)` returns the current number of Ps without changing it, and `runtime.NumCPU()` returns the logical CPUs the process may use — printing both at startup is the cheapest orientation you can get. ## The misconceptions to avoid - **A P is not a CPU core.** It is a runtime bookkeeping object. Its count often *defaults* from the available CPU count, which is why the two are so easily conflated. - **A goroutine is not a thread.** Creating one does not create an M. - **`GOMAXPROCS` is not a goroutine limit** and not a thread limit; it is the P count. - **The kernel never sees a goroutine.** It schedules Ms; the Go runtime schedules Gs onto them.

  • Which of the three does the operating system actually schedule?
    Only the M. Ms are real OS threads, so the kernel schedules them, delivers signals to them and shows them in thread listings. Gs and Ps are pure Go runtime structures with no kernel counterpart, which is why a host-level thread tool can never show you goroutines, and why switching between goroutines costs no system call.
  • Does `go f()` create an OS thread?
    No. It allocates or reuses a `g`, sets up its stack and entry point, and marks it runnable so some P will hand it to some M later. Threads are created by the runtime on its own schedule, independently of how many goroutines you start — that is exactly why goroutines are cheap enough to create per request.
  • What decides how many Ps a program has?
    `GOMAXPROCS`. The runtime derives a default at startup from the CPUs the process may use, and on recent Go on Linux also from the container's CPU limit; you can override it with the `GOMAXPROCS` environment variable or by calling `runtime.GOMAXPROCS(n)`. `runtime.GOMAXPROCS(0)` reads the current value without changing it.

saying these in an interview costs you the question

  • Says P is a physical CPU core
  • Says each goroutine gets its own OS thread
  • Thinks GOMAXPROCS caps how many goroutines you may create
  • Swaps the letters: calls M the goroutine
  • Claims the kernel schedules goroutines directly
open as a page

When a goroutine blocks on a channel receive, what happens to the OS thread it was running on?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Nothing blocks at the operating-system level. Go's runtime parks the goroutine, marking it waiting and detaching it from the thread, then runs another runnable goroutine on that same thread. The thread keeps doing useful work.

open as a page

In Go, does one goroutine blocked in a file-read syscall stop the other goroutines from running?

level: juniorimportance: must knowfreq 62%

basics

~20 s

No. When a goroutine enters a system call that blocks, the Go runtime detaches its P (the scheduling context) from that OS thread, and another thread picks the P up and keeps running the remaining goroutines.

open as a page

When a goroutine calls time.Sleep, what does the Go runtime block, and what wakes it?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Nothing at the OS level blocks. The Go runtime parks that goroutine and records its wake time in a runtime timer heap, freeing the thread for other goroutines; when the deadline passes the goroutine becomes runnable again.

open as a page

What exactly does GOMAXPROCS limit in a Go program, and what does it not limit?

level: middleimportance: must knowfreq 68%

basics

~20 s

GOMAXPROCS sets the number of Ps, capping how many goroutines execute Go code at the same time. It does not cap how many goroutines you can create, and it is not a ceiling on the OS threads the runtime may create.

open as a page

What is a preemption safe point in Go, and how is a goroutine that never reaches one stopped?

level: middleimportance: must knowfreq 45%

basics

~20 s

A safe point is an instruction where the runtime knows the goroutine's stack layout well enough to stop it, in practice a function prologue's stack check. Go 1.14 added asynchronous preemption, interrupting the thread with SIGURG when no safe point is reached.

open as a page

How does a Go P look for work when its own local run queue is empty?

level: middleimportance: must knowfreq 48%

basics

~20 s

It drains a batch from the global run queue, polls the network poller, then steals: up to four randomised passes over the other Ps, taking about half of a victim's local run queue. Only after all of that does its thread park.

open as a page

What happens when you send SIGQUIT (Ctrl-\) to a running Go program?

level: juniorimportance: should knowfreq 44%

basics

~20 s

By default the Go runtime prints a stack trace for every goroutine to standard error and then dies from the signal. It is a one-shot diagnostic: the process is killed, and deferred functions never run.

open as a page

Does a goroutine running a tight CPU-bound loop need runtime.Gosched() to let other goroutines run?

level: juniorimportance: should knowfreq 40%

basics

~20 s

No. Since Go 1.14 the runtime interrupts a long-running goroutine on its own using a signal, so a tight loop no longer starves the rest of the program. runtime.Gosched() yields voluntarily and is rarely needed.

open as a page

When you start a goroutine with go f(), where does the Go runtime put it to wait?

level: juniorimportance: should knowfreq 38%

basics

~20 s

The new goroutine goes into the runnext slot of the P that ran the go statement, bumping any goroutine already there onto that P's 256-entry local run queue. When that queue is full, half of it spills to the global run queue.

open as a page

What does runtime.NumGoroutine() count, and what does it deliberately leave out?

level: middleimportance: should knowfreq 52%

basics

~20 s

runtime.NumGoroutine returns how many goroutines exist right now in any state: running, runnable, sleeping or blocked. It excludes goroutines that have already finished and the runtime's own system goroutines, and the value is a snapshot that can be stale the moment it returns.

open as a page

In Go's scheduler, what does a P own, and why must an M acquire one to run Go code?

level: middleimportance: should knowfreq 42%

basics

~20 s

A P owns the state needed to run goroutines: a queue of ready goroutines and per-P caches such as the memory-allocation cache. Requiring an M to hold a P keeps Go-code parallelism at exactly GOMAXPROCS while the thread count varies freely.

open as a page

What does Go's runtime store on a channel so it knows which goroutine to wake?

level: middleimportance: should knowfreq 34%

basics

~20 s

Go's runtime queues a wait record, called a sudog, for each blocked goroutine: it names the goroutine and the address the value goes to. A counterpart operation pops the first record from that channel's FIFO queue and readies its goroutine.

open as a page

In Go's runtime, what preempts a goroutine that has run too long, and after roughly how long?

level: middleimportance: should knowfreq 32%

basics

~20 s

sysmon, the runtime's background monitor, checks each processor periodically and flags any goroutine that has held one for more than about 10 milliseconds, so the scheduler preempts it. A stop-the-world request preempts every running goroutine at once.

open as a page

What is the runnext slot in Go's per-P scheduler, and what goes into it?

level: middleimportance: should knowfreq 30%

basics

~20 s

runnext is a single-goroutine fast slot on each P, holding the goroutine that P most recently made runnable. The P runs it ahead of its entire local run queue, and it inherits the remaining time slice rather than getting a fresh one.

open as a page

How does Go's netpoller let a blocking-looking net.Conn.Read not block an OS thread?

level: middleimportance: should knowfreq 55%

basics

~20 s

Go opens network sockets in non-blocking mode and registers them with an event poller (epoll on Linux, kqueue on the BSDs, IOCP on Windows). A read with no data parks the goroutine, not the thread, and readiness makes the goroutine runnable again.

open as a page

If a goroutine calls runtime.LockOSThread and exits without unlocking, what happens to that OS thread?

level: middleimportance: should knowfreq 36%

basics

~20 s

The runtime terminates that OS thread instead of reusing it. That is deliberate: a thread whose per-thread state was changed must not go on to run other goroutines, so exiting while still locked is the documented way to discard it.

open as a page

Where does the Go runtime keep pending timers, and what checks that they are due?

level: middleimportance: should knowfreq 32%

basics

~20 s

Pending timers live in per-P min-heaps ordered by expiry, not one global list. Go's goroutine scheduler fires due ones while finding work, and a thread about to idle blocks in the netpoller until the earliest deadline.

open as a page

A daemon's goroutine count climbs every tick and never falls. What do GODEBUG=schedtrace and a SIGQUIT dump each tell you?

level: seniorimportance: should knowfreq 36%

basics

~20 s

schedtrace separates busy from stuck: idle Ps with empty run queues prove the extra goroutines are parked, not starved for CPU. Adding scheddetail=1 gives a line per goroutine with its wait reason, and a SIGQUIT dump gives each one's stack and how long it has been blocked.

open as a page

A containerized Go service reports a much lower GOMAXPROCS after only a toolchain upgrade. What changed, and how do you confirm it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Since Go 1.25 on Linux the default GOMAXPROCS is the lower of the usable CPU count and the container's cgroup CPU limit, so a rebuild on a newer toolchain gets far fewer Ps. Confirm by logging runtime.GOMAXPROCS(0) beside runtime.NumCPU().

open as a page

A multiplexer goroutine has been parked on a channel send for hours. What could ever wake it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Only another goroutine receiving from that channel, or closing it. A park carries no timeout and Go's scheduler never revisits waiting goroutines, so once the caller has gone, the send stays parked for the life of the process.

open as a page

Your Go worker pool's p99 is uneven across workers while a CPU sits idle — what in the run queues explains it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Work concentrates on the P that created it. A worker woken by a send on the jobs channel becomes runnable on the sender's P, so idle Ps get work only by stealing, which costs a search and takes the runnext slot last.

open as a page

A Go sidecar with GOMAXPROCS=4 runs 90 OS threads — what explains it, and what do you change?

level: seniorimportance: should knowfreq 40%

basics

~20 s

GOMAXPROCS bounds goroutines running Go code, not OS threads. Every goroutine parked in a blocking system call or a C call holds its own thread while the runtime spins up more to keep the scheduling contexts busy. Cap the blocking work, do not tune the scheduler.

open as a page

A Go tool calls setns(2) to enter a namespace, then its work sometimes runs in the wrong namespace. Why, and what is the fix?

level: seniorimportance: should knowfreq 30%

basics

~20 s

The setns call changes only the calling OS thread, and the Go scheduler may resume the goroutine on a different thread after a syscall or a preemption. Pin with runtime.LockOSThread before entering, and let that goroutine exit rather than unlocking.

open as a page

Go 1.23 rewrote the runtime's timers: what changed observably for time.Timer and time.Ticker?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Two things. Unreferenced timers and tickers became ordinary garbage, collectable even if Stop was never called. And their channels became unbuffered, so Stop and Reset now guarantee no value prepared before the call is delivered after it.

open as a page

Should a shared library call runtime.LockOSThread internally, or push thread pinning onto its callers?

level: principalimportance: should knowfreq 20%

basics

~20 s

Decide by how far the thread affinity reaches. If the whole affected operation happens inside one of your calls, pin internally and hide it. If the affinity outlives the call or caller code runs inside the pinned region, make pinning a documented obligation on the caller.

open as a page

What does runtime.LockOSThread do, and when does a Go program actually need it?

level: juniorimportance: nice to knowfreq 28%

basics

~20 s

runtime.LockOSThread wires the calling goroutine to the OS thread it is running on: that goroutine runs only there, and no other goroutine runs on that thread. You need it for APIs that keep state per OS thread.

open as a page

In a GODEBUG=schedtrace=1000 line, what do `runqueue=12` and the trailing `[3 0 2 0]` mean?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

runqueue is the length of the scheduler's single global run queue, and the bracketed list gives each P's local run queue length, one entry per P. Both count goroutines that are runnable but not currently running; blocked goroutines appear in neither.

open as a page

When a sender finds a receiver already parked on a channel, where is the value copied?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

The sending goroutine copies the value straight into the parked receiver's destination variable, using the address in that receiver's wait record, then marks the receiver runnable. The channel's buffer is not used, even on a buffered channel.

open as a page

Why does reading a regular file in Go tie up an OS thread when reading a socket does not?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Readiness notification does not work for regular files — the kernel will not usefully poll them, and non-blocking mode has no effect. So Go issues a real blocking read that occupies its OS thread, while sockets are registered with the poller and only park a goroutine.

open as a page

showing 1–30 of 35