Es gibt das Geruecht, dass die Go Runtime mehrere Betriebssystem-Threads vorab erzeugt, noch bevor dein Programm echte Arbeit macht.
Ich wollte eine konkretere Antwort: Wie viele Threads sind mit GOMAXPROCS=1, ohne eigene Goroutines und mit einer kleinen Busy Loop wirklich am Leben?
Die kurze Antwort: In diesem Experiment waren es mindestens drei OS Threads.
Die nuetzlichere Antwort ist, warum diese Threads existieren.
Testprogramm
Das Programm bleibt absichtlich langweilig:
package main
import "runtime"
func main() {
runtime.GOMAXPROCS(1) // keep a single P
var values []int
for i := 0; i < 10_000_000_000; i++ {
values = append(values, i)
}
}
Es gibt keine eigenen Goroutines und keine explizite Concurrency. Die Schleife haelt den Prozess nur lange genug beschaeftigt, um den Scheduler zu beobachten.
Erster Blick mit schedtrace
Baue und starte das Programm mit Scheduler Tracing:
go build -o threads
GOMAXPROCS=1 GODEBUG=schedtrace=1000 ./threads
Die Runtime schreibt alle 1000 ms einen Scheduler Snapshot.
Auch mit nur einem Processor (P) zeigt der Trace mehr als einen Thread (M). In diesem Setup erscheinen drei OS Threads.
Das ist weniger als die oft erzaehlten fuenf, aber trotzdem mehr als das naive mentale Modell von "single-threaded".
Mehr Details mit scheddetail
Aktiviere detaillierte Scheduler-Ausgabe:
GOMAXPROCS=1 GODEBUG="schedtrace=1000,scheddetail=1" ./threads
Jetzt zeigt der Output einen Processor, normalerweise P0, und mehrere Runtime Goroutines fuer Garbage Collection und Background Maintenance.
Man sieht Helfer wie Sweeper, Scavenger und die Force-GC-Goroutine. Das ist Runtime-Servicearbeit, keine User-Arbeit.
Der wichtige Punkt: Auch wenn du selbst keine Goroutines schreibst, hat die Runtime interne Aufgaben zu verwalten.
Was passiert, wenn GC deaktiviert ist?
Deaktiviere automatische Garbage Collection:
GOGC=off GOMAXPROCS=1 GODEBUG="schedtrace=1000,scheddetail=1" ./threads
Die Thread-Anzahl bleibt bei drei.
Automatische GC zu deaktivieren veraendert, wann Collections passieren. Die Runtime Helper Goroutines existieren trotzdem. Sie koennen geparkt sein, gehoeren aber zur Struktur der Runtime.
Woher kommen die zusaetzlichen Threads?
Ein Blick in den Runtime Code zeigt einige Quellen:
- der System Monitor Thread, oft
sysmongenannt, - der Main Runtime Thread, der am aktiven Processor haengt,
- ein Spare Thread, mit dem die Runtime bei Handoffs oder blockierenden Operationen weiterlaufen kann.
In Go Scheduler Begriffen laufen Goroutines (G) auf logischen Processors (P), und Processors werden von OS Threads (M) ausgefuehrt.
Mit GOMAXPROCS=1 gibt es einen aktiven P. Das bedeutet aber nicht, dass der Prozess exakt einen OS Thread besitzt.
Eine Runtime-Hypothese testen
Ein lokaler experimenteller Runtime Build, bei dem ein Startup Lock entfernt wurde, zeigte nur zwei Threads.
Das ist nichts fuer Produktion. Der Punkt des Experiments ist nicht, Go zu patchen, sondern die Ursache des zusaetzlichen Threads zu bestaetigen.
Das Ergebnis stuetzt die Idee, dass die Runtime mit einer kleinen Baseline startet und einen Spare M fuer Koordination bereithaelt.
Blockierende Syscalls machen den Spare Thread nuetzlich
Fuege eine Goroutine hinzu, die in einem Syscall blockiert:
go func() {
unix.Read(unix.Stdin, make([]byte, 1)) // blocks on stdin
}()
Nach dem Start hat der Prozess weiterhin nur wenige Threads, aber die Rollen werden klarer:
- ein Thread kann in
readblockieren, - ein anderer kann
P0weiter antreiben, - ein weiterer kann als System Monitor arbeiten.
Genau deshalb ist der Spare nuetzlich. Wenn eine Goroutine im Kernel blockiert, braucht Go weiterhin einen Weg, runnable Work voranzubringen.
Key Findings
Mit GOMAXPROCS=1 zeigte dieses Experiment drei OS Threads.
Jeder zusaetzliche aktive P kann einen eigenen M brauchen, meist lazy erzeugt.
GC Helper Goroutines existieren auch dann, wenn automatische GC deaktiviert ist. Sie schlafen, bis sie gebraucht werden.
Die Runtime erzeugt nicht gierig einen Haufen unnoetiger Worker. Sie behaelt Threads, die bereits gestartet wurden, wenn sie fuer Latenz und Koordination nuetzlich sind.
Warum das wichtig ist
Fuer normale Anwendungen sind zwei oder drei Basis-Threads kein Problem. Der zusaetzliche Stack-Speicher ist meistens klein im Vergleich zu den Vorteilen fuer Zuverlaessigkeit und Latenz.
In engen Containern, auf kleinen Embedded Boards oder bei sehr hoher Prozessdichte ist diese Baseline aber relevant. "Ein Go Prozess" bedeutet nicht automatisch "ein OS Thread", selbst mit GOMAXPROCS=1.
Der praktische Takeaway fuer 2026: Go's Scheduler ist effizient, aber es bleibt eine Runtime mit internen Services, Background Coordination und einer kleinen Crew von OS Threads, die den Prozess reaktionsfaehig haelt.


