Go wurde ursprünglich für das Schreiben von leistungsstarken Netzwerk- und Parallelprogrammen entwickelt, daher wurde besonderes Augenmerk auf den eingebauten Scheduler und das einfache Management von Parallelität mit Goroutinen gelegt. Im Gegensatz zu den meisten Sprachen, in denen die Betriebssystem-Threads direkt vom Benutzer der Sprache verwaltet werden, sind Goroutinen in Go leichter, Tausende von ihnen können gleichzeitig über eine feste Anzahl von Betriebssystem-Threads arbeiten.
Problem: Die direkte Verwaltung von Threads ist komplex: Sie führt zu schneller Ressourcennutzung, Datenrennen und Schwierigkeiten im Speicher-Management.
Lösung: Go verwendet das M:N-Modell — eine große Anzahl von Goroutinen (M) wird auf eine begrenzte Anzahl von Betriebssystem-Threads (N) multiplexiert. Dafür ist der Scheduler verantwortlich, der auf der Ebene des Go-Runtime implementiert ist und automatisch die Ausführung von Goroutinen ausbalanciert und umverteilt. Der Programmierer verwaltet nur die Ausführung und Synchronisation von Goroutinen, nicht die Betriebssystem-Threads direkt.
Beispielcode:
package main import ( "fmt" "time" ) func worker(id int) { fmt.Printf("Worker %d startet ", id) time.Sleep(time.Second) fmt.Printf("Worker %d fertig ", id) } func main() { for i := 0; i < 5; i++ { go worker(i) } time.Sleep(2 * time.Second) }
Wesentliche Merkmale:
Wird jede Goroutine konkurrierend auf einem separaten CPU-Kern ausgeführt?
Nein. Goroutinen werden multiplexiert und der Scheduler bestimmt die tatsächliche Anzahl der gleichzeitig ausgeführten Aufgaben.
Kann man die Ausführung bestimmter Goroutinen/Threads manuell steuern?
Nein. Das Go-Runtime bietet keine Schnittstelle für die direkte Planung. Eine Ausnahme ist GOMAXPROCS zur Festlegung der Anzahl der Betriebssystem-Threads.
Bedeutet eine große Anzahl von Goroutinen eine automatische Beschleunigung des Programms?
Nein. Eine hohe Anzahl konkurrierender Operationen kann zu zusätzlichen Overheads führen: Kontextwechsel, Ressourcenkonflikte, erhöhten GC-Zeiten und speicherverbrauch.
Ein Mikrodienst verarbeitete eingehende Anfragen parallel, ohne die Anzahl der Goroutinen zu begrenzen und wartete nicht mit WaitGroup auf das Ende — Ergebnis: erhöhte Antwortzeiten, Datenrennen, unvorhersehbare Zeitüberschreitungen.
Vorteile:
Nachteile:
Ein Arbeiter-Manager wurde über einen Goroutine-Pool realisiert, die Begrenzung der gleichzeitig arbeitenden Aufgaben erfolgt über ein Semaphore oder Kanäle. WaitGroup wartet korrekt auf den Abschluss aller Aufgaben.
Vorteile:
Nachteile: