Vol. 9 — № 03
The Go Loop · KiosqueNewsstand
Blog  
Un atelier Go · Édition d'ApprentissageA Go Workshop · Learning Edition

Le scheduler The scheduler

Des centaines de milliers de goroutines tiennent sur une poignée de threads OS — c'est le scheduler M:N qui les fait danser. GOMAXPROCS, préemption, et la vieille histoire de la goroutine en boucle serrée qui affamait les autres (et pourquoi c'est presque fini). Hundreds of thousands of goroutines ride on a handful of OS threads — the M:N scheduler makes them dance. GOMAXPROCS, preemption, and the old tale of the tight-loop goroutine that starved the others (and why that's nearly over).

AudienceAudience
Dev qui déploie des services Go dans des containers, ou qui veut comprendre comment le runtime gère les goroutines Dev deploying Go services in containers, or who wants to understand how the runtime manages goroutines
Format
Self-paced
ChapitresChapters
5
Date
Août 2028 Aug 2028
≈ 17 min ●●●● SchedulerGOMAXPROCSPréemption

Chapitre 1 en accès libre — la suite (ch. 2 à 5) est réservée. Chapter 1 free to read — the rest (ch. 2–5) is members-only.

01CadrageFraming3 min

Des millions de goroutines sur quelques threads OS. Le scheduler M:N de Go les fait danser — GOMAXPROCS contrôle combien peuvent danser en même temps.Millions of goroutines on a few OS threads. Go's M:N scheduler makes them dance — GOMAXPROCS controls how many can dance at once.

Le volume 1 a posé les goroutines comme unité de concurrence légère. Ce dernier numéro répond à la question laissée ouverte : comment des centaines de milliers de goroutines peuvent-elles coexister sur une poignée de threads OS ? La réponse est le scheduler M:N — M goroutines sur N threads OS — implémenté par le runtime Go. La structure du scheduler repose sur trois entités. Les G (goroutines) sont les unités de travail légères : chaque goroutine démarre avec environ 2 Ko de pile et peut atteindre 1 Go par croissance dynamique. Les M (machines) sont les threads OS : créés par le runtime, ils peuvent être bien plus nombreux que GOMAXPROCS car un M bloqué sur un syscall est remplacé par un nouveau M pour ne pas bloquer les autres goroutines. Les P (processeurs) sont les contextes d'exécution : il en existe exactement GOMAXPROCS, et chaque M qui veut exécuter des G doit en acquérir un. GOMAXPROCS est donc le vrai levier de parallélisme : il contrôle combien de goroutines peuvent s'exécuter simultanément sur des cœurs différents. Par défaut depuis Go 1.5, GOMAXPROCS vaut le nombre de CPUs logiques — le bon défaut dans la plupart des cas, sauf dans des environnements containerisés où les CPUs disponibles ne sont pas ceux de la machine hôte — un angle mort que Go 1.25 a enfin comblé en lisant le quota CPU du cgroup sous Linux.Volume 1 introduced goroutines as lightweight concurrency units. This last issue answers the question left open: how can hundreds of thousands of goroutines coexist on a handful of OS threads? The answer is the M:N scheduler — M goroutines on N OS threads — implemented by the Go runtime. The scheduler's structure rests on three entities. Gs (goroutines) are lightweight work units: each goroutine starts with about 2KB of stack and can reach 1GB through dynamic growth. Ms (machines) are OS threads: created by the runtime, they can be far more numerous than GOMAXPROCS because a blocked M (on a syscall) is replaced by a new M to not block other goroutines. Ps (processors) are execution contexts: there are exactly GOMAXPROCS of them, and each M that wants to run Gs must acquire one. GOMAXPROCS is therefore the real parallelism lever: it controls how many goroutines can run simultaneously on different cores. By default since Go 1.5, GOMAXPROCS equals the number of logical CPUs — the right default in most cases, except in containerized environments where available CPUs don't match the host machine's — a blind spot Go 1.25 finally closed by reading the cgroup CPU quota on Linux.

La structure G/M/P et GODEBUG=schedtraceThe G/M/P structure and GODEBUG=schedtrace
// Le scheduler Go : G sur M via P
//
// G (Goroutine) : unité de travail légère — ~2KB de pile initiale
// M (Machine)   : thread OS — créé par le runtime, bloque sur les syscalls
// P (Processor) : contexte d'exécution — exactement GOMAXPROCS P existent
//
// Chaque P a une run queue locale de G à exécuter.
// Un M doit acquérir un P pour exécuter des G.
// Si un M bloque (syscall, cgo), un autre M prend le P et continue.

// Voir l'état du scheduler en temps réel :
GOMAXPROCS=4 GODEBUG=schedtrace=1000 go run ./server.go
// toutes les 1000ms : goroutines en cours, en attente, threads, ...
// SCHED 1000ms: gomaxprocs=4 idleprocs=2 threads=6 spinningthreads=1 runqueue=3 ...
G / M / P — la trinité du schedulerG / M / P — the scheduler trinity
G
Goroutine
unité de travail · ~2KB · millions possibles
M
Machine
thread OS · bloque sur syscalls · N > GOMAXPROCS
P
Processor
contexte d'exécution · exactement GOMAXPROCS

Un M doit acquérir un P pour exécuter des G. GOMAXPROCS P → au plus GOMAXPROCS G en parallèle. Les G en attente sont dans les run queues — locales à chaque P, ou globale.An M must acquire a P to run Gs. GOMAXPROCS P → at most GOMAXPROCS Gs in parallel. Waiting Gs are in run queues — local to each P, or global.

Le réflexe du numéroThe issue's reflex

« Mes goroutines ne progressent pas en parallèle — est-ce un bug du scheduler ? » Presque toujours non. La vraie question est : GOMAXPROCS est-il bien configuré pour l'environnement d'exécution ? Dans un container avec limite CPU de 0.5 core, GOMAXPROCS=runtime.NumCPU() lit le nombre de CPUs de l'hôte (32, 64...) et crée 32 P — dont 31 sont inutiles. Avant Go 1.25, la bibliothèque uber-go/automaxprocs corrige ça en lisant les CFS quotas Linux ; depuis Go 1.25, le runtime le fait lui-même."My goroutines don't progress in parallel — is it a scheduler bug?" Almost never. The real question is: is GOMAXPROCS properly configured for the execution environment? In a container with a 0.5 core CPU limit, GOMAXPROCS=runtime.NumCPU() reads the host's CPU count (32, 64...) and creates 32 P — 31 of which are useless. Before Go 1.25, the uber-go/automaxprocs library fixes this by reading Linux CFS quotas; since Go 1.25, the runtime does it itself.

Pourquoi M peut être > GOMAXPROCSWhy M can be > GOMAXPROCS

Quand une goroutine fait un syscall bloquant (lecture fichier, appel réseau lent), le M qui l'exécute bloque avec elle. Pour ne pas laisser le P inutilisé, le runtime détache le P du M bloqué et le donne à un autre M (ou en crée un nouveau). Résultat : le nombre de threads OS (M) peut dépasser GOMAXPROCS quand beaucoup de goroutines font des I/O bloquants simultanément. La limite de M se règle avec debug.SetMaxThreads (défaut : 10 000). Les goroutines utilisant des I/O non-bloquants (net.Conn avec le network poller) ne créent pas de nouveaux M — elles se parquent dans le poller et le M est libéré.When a goroutine makes a blocking syscall (file read, slow network call), the M running it blocks with it. To not leave the P unused, the runtime detaches the P from the blocked M and gives it to another M (or creates a new one). Result: the number of OS threads (M) can exceed GOMAXPROCS when many goroutines make blocking I/O simultaneously. The M limit is set with debug.SetMaxThreads (default: 10,000). Goroutines using non-blocking I/O (net.Conn with the network poller) don't create new Ms — they park in the poller and the M is released.

🔒

La suite est réservée The rest is members-only

Le premier numéro est libre. Débloque tout The Go Loop — tous les volumes, à vie — pour 5 €, paiement unique. The first issue is free. Unlock all of The Go Loop — every volume, forever — for €5, one-time.

Retour au kiosqueBack to newsstand