Grundlagen von Goroutinen
Goroutine
Wenn man Gopher fragt, die Vorteile von Golang zu erläutern, stößt man häufig auf Artikel über Concurrency. Die Grundlage dieser Inhalte bildet die leichtgewichtige und unkompliziert zu handhabende Goroutine. Hierzu habe ich eine kurze Abhandlung verfasst.
Concurrency vs. Parallelism
Bevor wir Goroutines verstehen, möchte ich zwei oft verwechselte Konzepte erläutern.
- Concurrency: Concurrency befasst sich damit, viele Aufgaben gleichzeitig zu bearbeiten. Dies bedeutet nicht unbedingt, dass sie tatsächlich gleichzeitig ausgeführt werden, sondern ist ein strukturelles und logisches Konzept, das dem Benutzer den Anschein vermittelt, als würden mehrere Aufgaben gleichzeitig verarbeitet, indem sie in kleine Einheiten unterteilt und abwechselnd ausgeführt werden. Concurrency ist auch auf Single-Core-Systemen möglich.
- Parallelism: Parallelism bedeutet, „mehrere Aufgaben auf mehreren Cores gleichzeitig zu bearbeiten“. Es handelt sich buchstäblich um eine parallele Arbeitsweise, bei der verschiedene Aufgaben gleichzeitig ausgeführt werden.
Goroutines ermöglichen die einfache Implementierung von Concurrency über den Go-Runtime-Scheduler und nutzen Parallelism auf natürliche Weise durch die GOMAXPROCS
-Einstellung.
Das Multi-Threading in Java, das häufig und intensiv genutzt wird, ist ein typisches Beispiel für Parallelism.
Warum sind Goroutines vorteilhaft?
Lightweight
Die Erstellungskosten sind im Vergleich zu anderen Sprachen sehr gering. Hier stellt sich die Frage, warum Golang weniger davon verwendet: Der Grund liegt darin, dass die Erstellung intern von der Go-Runtime verwaltet wird. Dies liegt daran, dass es sich um einen leichtgewichtigen logischen Thread handelt, der kleiner als eine OS-Thread-Einheit ist. Der initiale Stack benötigt etwa 2 KB Speicherplatz und kann je nach Benutzerimplementierung dynamisch durch Hinzufügen von Stack-Speicher variabel angepasst werden.
Da die Verwaltung auf Stack-Ebene erfolgt, sind Erstellung und Entfernung sehr schnell und kostengünstig, sodass die Ausführung von Millionen von Goroutines keine Belastung darstellt. Dies ermöglicht es Goroutines, die OS-Kernel-Intervention dank des Runtime-Schedulers zu minimieren.
Performance
Zunächst ermöglicht Goroutine, wie oben beschrieben, eine schnelle Aufgabenumschaltung, da die OS-Kernel-Intervention gering ist und der Context Switching auf User-Level-Ebene kostengünstiger ist als auf OS-Thread-Ebene.
Darüber hinaus werden OS-Threads mithilfe des M:N-Modells zugewiesen und verwaltet. Durch die Erstellung eines OS-Thread-Pools können Aufgaben mit weniger Threads bearbeitet werden, ohne dass eine große Anzahl von Threads erforderlich ist. Wenn beispielsweise eine Goroutine in einen Wartezustand wie einen System Call gerät, führt die Go-Runtime eine andere Goroutine auf dem OS-Thread aus, sodass der OS-Thread nicht untätig bleibt und die CPU effizient für eine schnelle Verarbeitung genutzt werden kann.
Dies trägt dazu bei, dass Golang insbesondere bei I/O-Operationen eine höhere Performance als andere Sprachen erzielen kann.
Concise
Ein großer Vorteil ist auch die einfache Handhabung von Funktionen mit dem go
-Keyword, wenn Concurrency erforderlich ist.
Komplexe Locks wie Mutex
und Semaphore
müssten verwendet werden, und die Verwendung von Locks erfordert zwangsläufig die Berücksichtigung von Deadlock-Zuständen, was bereits in der Entwurfsphase vor der Entwicklung eine komplexe Phase notwendig macht.
Goroutine fördert die Datenübertragung über Channel
gemäß der Philosophie „Nicht durch das Teilen von Speicher kommunizieren, sondern durch Kommunikation Speicher teilen“, und SELECT
unterstützt sogar die Funktion, Kanäle zu verarbeiten, sobald Daten bereit sind, in Kombination mit Channel
. Darüber hinaus ermöglicht sync.WaitGroup
ein einfaches Warten, bis alle Goroutines abgeschlossen sind, wodurch der Arbeitsablauf leicht verwaltet werden kann. Dank dieser Tools können Datenkonflikte zwischen Threads vermieden und Concurrency sicherer verarbeitet werden.
Darüber hinaus kann durch die Verwendung von context
auf User-Level-Ebene die Lebensdauer, Abbruch, Timeout, Deadline und der Request-Scope gesteuert werden, wodurch ein gewisses Maß an Stabilität gewährleistet werden kann.
Parallele Ausführung von Goroutines (GOMAXPROCS)
Obwohl die Vorteile der Concurrency von Goroutines erwähnt wurden, fragen Sie sich vielleicht, ob Parallelism nicht unterstützt wird. Die Anzahl der Cores in modernen CPUs hat sich im Gegensatz zur Vergangenheit auf zweistellige Zahlen erhöht, und auch Heim-PCs verfügen über eine beträchtliche Anzahl von Cores.
Goroutine führt jedoch auch parallele Aufgaben aus, und zwar mittels GOMAXPROCS
.
Wenn GOMAXPROCS
nicht konfiguriert ist, variiert die Einstellung je nach Version:
Vor 1.5: Standardwert 1. Wenn mehr als 1 benötigt wurde, war eine Einstellung wie
runtime.GOMAXPOCS(runtime.NumCPU())
zwingend erforderlich.1.5 bis 1.24: Der Wert wurde auf die Anzahl aller verfügbaren logischen Cores geändert. Ab diesem Zeitpunkt war eine Einstellung durch den Entwickler nicht mehr erforderlich, es sei denn, es gab spezielle Einschränkungen.
1.25: Als Sprache, die in Container-Umgebungen populär ist, wird die
CPU-Begrenzung
im Container, die durch cGroup unter Linux festgelegt ist, überprüft.Wenn also die Anzahl der logischen Cores 10 beträgt und die CPU-Begrenzung 5 ist, wird
GOMAXPROCS
auf den niedrigeren Wert von 5 gesetzt.
Die Änderung in Version 1.25 stellt eine sehr bedeutende Neuerung dar, da sie die Verwendbarkeit der Sprache in Container-Umgebungen erheblich verbessert hat. Dies hat die unnötige Thread-Erstellung und den Context Switching reduziert und somit CPU-Throttling verhindert.
1package main
2
3import (
4 "fmt"
5 "math/rand"
6 "runtime"
7 "time"
8)
9
10func exe(name int, wg *sync.WaitGroup) {
11 defer wg.Done()
12
13 fmt.Printf("Goroutine %d: Start\n", name) // Goroutine %d: Start
14 time.Sleep(10 * time.Millisecond) // Verzögerung zur Simulation der Arbeit
15 fmt.Printf("Goroutine %d: Ende\n", name) // Goroutine %d: Ende
16}
17
18func main() {
19 runtime.GOMAXPROCS(2) // Nur 2 CPU-Kerne verwenden
20 wg := sync.WaitGroup();
21 goroutineCount := 10
22 wg.Add(goroutineCount)
23
24 for i := 0; i < goroutineCount; i++ {
25 go exe(i, &wg)
26 }
27
28 fmt.Println("Warte, bis alle Goroutinen beendet sind...") // Warte, bis alle Goroutinen beendet sind...
29 wg.Wait()
30 fmt.Println("Alle Aufgaben sind abgeschlossen.") // Alle Aufgaben sind abgeschlossen.
31
32}
33
Goroutine Scheduler (M:N Modell)
Der vorhergehende Abschnitt, der besagt, dass OS-Threads mit dem M:N-Modell zugewiesen und verwaltet werden, kann noch spezifischer mit dem Goroutine GMP-Modell erläutert werden.
- G (Goroutine): Die kleinste Arbeitseinheit, die in Go ausgeführt wird.
- M (Machine): OS-Thread (tatsächlicher Arbeitsort).
- P (Processor): Logischer Prozessor, der von der Go-Runtime verwaltet wird.
P verfügt zusätzlich über eine Local Run Queue und fungiert als Scheduler, der zugewiesene Gs Ms zuweist. Einfach ausgedrückt, Goroutines sind
Der Arbeitsablauf von GMP ist wie folgt:
- Wenn eine G (Goroutine) erstellt wird, wird sie der Local Run Queue von P (Processor) zugewiesen.
- P (Processor) weist die G (Goroutine) aus der Local Run Queue einem M (Machine) zu.
- M (Machine) gibt den Zustand der G (Goroutine) zurück: blockiert, abgeschlossen, präemptiert.
- Work-Stealing: Wenn die Local Run Queue eines P leer wird, überprüft ein anderes P die Global Queue. Wenn dort auch keine G (Goroutine) vorhanden ist, stiehlt es die Arbeit eines anderen lokalen P (Processor), um sicherzustellen, dass alle Ms ununterbrochen arbeiten.
- System Call Handling (Blocking): Wenn eine G (Goroutine) während der Ausführung blockiert wird, geht M (Machine) in den Wartezustand über. In diesem Fall trennt P (Processor) sich von der blockierten M (Machine) und verbindet sich mit einer anderen M (Machine), um die nächste G (Goroutine) auszuführen. Dabei gibt es keine CPU-Verschwendung während der Wartezeit bei I/O-Operationen.
- Wenn eine G (Goroutine) zu lange präemptiert wird, wird anderen Gs (Goroutines) die Ausführungsmöglichkeit gegeben.
Der Golang GC (Garbage Collector) läuft ebenfalls auf Goroutines und kann den Speicher parallel bereinigen, während die Ausführung der Anwendung nur minimal unterbrochen wird (STW), wodurch Systemressourcen effizient genutzt werden.
Zuletzt ist Golang einer der großen Vorteile der Sprache, und es gibt noch viele weitere. Ich hoffe, dass viele Entwickler Freude an Go haben werden.
Vielen Dank.