Fundamentals 58 min read

Deep Dive into Go's GMP Scheduler: G, M, P Model and Source Code Walkthrough

This article provides an in‑depth analysis of Go’s GMP scheduler, explaining the evolution of the G‑M‑P model, detailing the structures g, m, p, their initialization, scheduling loops, code paths for goroutine creation, execution, and termination, and illustrating key source snippets with explanations.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Deep Dive into Go's GMP Scheduler: G, M, P Model and Source Code Walkthrough

The article begins by introducing the need for a more efficient scheduler in Go, describing how the classic GM model suffered from global lock contention and how the introduction of the P (processor) created the modern GMP model, which uses a global queue and per‑processor local queues to improve concurrency.

It then explains the three core structures:

type g struct { ... } – represents a goroutine, storing its stack, registers, state, and identifiers.

type m struct { ... } – represents a worker thread, holding a scheduling stack (g0), TLS, and links to its bound P.

type p struct { ... } – represents a logical processor, containing a local run queue (runq), status flags, and a link to its owning M.

The schedt structure is introduced as the global scheduler, holding the global run queue, idle M/P lists, and various counters used for load balancing and preemption.

Goroutine creation is traced step‑by‑step: the go keyword compiles to a call to runtime.newproc , which invokes newproc1 to allocate a g , set up its gobuf (stack pointer, program counter, and return address), and finally place it into either the local run queue or the global queue via runqput . The article details how the return address is set to goexit+1 so that the goroutine cleans up correctly after finishing.

When a goroutine becomes runnable, the scheduler’s schedule loop runs on each M. It calls findRunnable to obtain a g , then execute binds the M and G, changes the G’s state to _Grunning , and finally jumps to user code with gogo(&gp.sched) . The gogo assembly routine swaps the TLS‑stored G, switches the CPU stack to the goroutine’s stack, restores saved registers, and jumps to the goroutine’s entry point.

When the goroutine finishes, it returns to goexit , which calls goexit1 , then mcall(goexit0) . The mcall assembly routine saves the current G’s state, switches back to the M’s g0 stack, and invokes goexit0(gp) . goexit0 marks the G as dead, clears its fields, returns it to the P’s free‑list, and re‑enters the scheduler loop.

Finally, the article summarizes the state transitions for G (idle → runnable → running → dead), M (spinning ↔ non‑spinning), and P (idle ↔ running), and outlines the overall scheduling strategy, including work‑stealing, hand‑off, and cooperative preemption.

concurrencyGoSchedulerRuntimeGMP
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.