Skip to content
Tech News
← Back to articles

Understanding the Go Runtime: The Scheduler

read original get Go Runtime → more articles
Why This Matters

This article explains the Go runtime scheduler, which efficiently manages the execution of millions of goroutines across limited CPU cores by multiplexing them onto a small number of OS threads. Understanding the GMP model—G (goroutine), M (OS thread), and P (processor)—is crucial to grasp how Go achieves concurrency and high performance. The scheduler's design ensures that goroutines are executed fairly and efficiently, making Go a powerful language for concurrent programming.

Key Takeaways

In the previous article we explored how Go’s memory allocator manages heap memory — grabbing large arenas from the OS, dividing them into spans and size classes, and using a three-level hierarchy (mcache, mcentral, mheap) to make most allocations lock-free. A key detail was that each P (processor) gets its own memory cache. But we never really explained what a P is, or how the runtime decides which goroutine runs on which thread. That’s the scheduler’s job, and that’s what we’re exploring today.

The scheduler is the piece of the runtime that answers a deceptively simple question: which goroutine runs next? You might have hundreds, thousands, or even millions of goroutines in your program, but you only have a handful of CPU cores. The scheduler’s job is to multiplex all those goroutines onto a small number of OS threads, keeping every core busy while making sure no goroutine gets starved.

If you’ve ever used goroutines and channels, you’ve already benefited from the scheduler without knowing it. Every go statement, every channel send and receive, every time.Sleep —they all interact with the scheduler. Let’s see how it works.

Let’s start with the fundamental building blocks — the three structures that the entire scheduler is built around.

The GMP Model

The scheduler is built around three concepts, commonly called the GMP model: G (goroutine), M (machine/OS thread), and P (processor). We touched on these during the bootstrap article, but now let’s look at them properly.

Let’s look at each one.

G — Goroutine

A G is a goroutine — the Go runtime’s representation of a piece of concurrent work. Every time you write go f() , the runtime creates (or reuses) a G to track that function’s execution.

What does a G actually carry? The struct has a lot of fields, but the ones I think are most useful for understanding how it works are: a small stack (starting at just 2KB), some saved registers (stack pointer, program counter, etc.) so the scheduler can pause it and resume it later, a status field that tracks what the goroutine is doing (running, waiting, ready to run), and a pointer to the M currently running it. The full struct in src/runtime/runtime2.go has a lot more — fields for panic and defer handling, GC assist tracking, profiling labels, timers, and more.

... continue reading