Grundlagen der Goroutinen
Goroutine
Wenn Gopher gebeten werden, die Vorteile von Golang zu erläutern, wird oft ein Artikel über Concurrency erwähnt. Die Grundlage dafür ist die Goroutine, die leicht und einfach zu handhaben ist. Dazu habe ich eine kurze Zusammenfassung erstellt.
Concurrency vs. Parallelism
Bevor wir Goroutinen verstehen, möchte ich zunächst 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. Es ist vielmehr ein strukturelles, logisches Konzept, bei dem mehrere Aufgaben in kleine Einheiten zerlegt und abwechselnd ausgeführt werden, sodass es für den Benutzer so aussieht, als würden mehrere Aufgaben gleichzeitig bearbeitet. Concurrency ist auch auf einem Einzelkernsystem möglich.
- Parallelism: Parallelism bedeutet, "mehrere Aufgaben gleichzeitig auf mehreren Cores zu bearbeiten". Es ist, wie der Name schon sagt, die parallele Ausführung von Aufgaben, wobei verschiedene Aufgaben gleichzeitig ausgeführt werden.
Goroutinen ermöglichen eine einfache Implementierung von Concurrency durch den Go-Laufzeit-Scheduler und nutzen Parallelism auf natürliche Weise durch die GOMAXPROCS-Einstellung.
Der Multi-thread-Ansatz von Java, der häufig verwendet wird, ist ein typisches Beispiel für Parallelism.
Warum sind Goroutinen gut?
lightweight
Die Erstellungskosten sind im Vergleich zu anderen Sprachen sehr gering. Hier stellt sich die Frage, warum Golang dies so sparsam nutzt. Der Grund ist, dass die Erstellung intern von der Go-Laufzeit verwaltet wird. Dies liegt daran, dass es sich um den oben genannten leichten logischen Thread handelt, der kleiner ist als eine OS-Thread-Einheit. Der initiale Stack benötigt etwa 2 KB Speicherplatz und kann je nach Benutzerimplementierung dynamisch durch Hinzufügen von Stack erweitert werden.
Da die Verwaltung auf Stack-Ebene erfolgt, ist die Erstellung und Entfernung sehr schnell und kostengünstig. Selbst Millionen von Goroutinen können ohne Belastung verarbeitet werden. Dadurch kann Goroutine dank des Laufzeit-Schedulers den Eingriff des OS-Kernels minimieren.
performance
Zunächst einmal können Goroutinen, wie oben beschrieben, Aufgaben schnell wechseln, da der Eingriff des OS-Kernels gering ist und der Kontextwechsel auf Benutzerebene (User-Level) kostengünstiger ist als auf OS-Thread-Ebene.
Darüber hinaus werden sie unter Verwendung des M:N-Modells OS-Threads zugewiesen und verwaltet. Durch die Erstellung eines OS-Thread-Pools können Aufgaben mit weniger Threads bearbeitet werden, ohne dass viele Threads erforderlich sind. Wenn beispielsweise ein Goroutine in einen Wartezustand gerät, wie bei einem Systemaufruf, führt die Go-Laufzeit andere Goroutinen auf dem OS-Thread aus, wodurch der OS-Thread nicht untätig bleibt und die CPU effizient genutzt wird, was eine schnelle Verarbeitung ermöglicht.
Dies führt dazu, dass Golang insbesondere bei I/O-Operationen eine höhere Leistung erzielen kann als andere Sprachen.
concise
Ein großer Vorteil ist auch, dass Funktionen bei Bedarf an Concurrency einfach mit dem go-Schlüsselwort bearbeitet werden können.
Komplexe Locks wie Mutex und Semaphore müssen verwendet werden, und bei der Verwendung von Locks muss zwangsläufig der DeadLock-Zustand berücksichtigt werden, was bereits in der Entwurfsphase vor der Entwicklung komplexe Schritte erforderlich macht.
Goroutine empfiehlt die Datenübertragung über Channel gemäß der Philosophie "Nicht durch das Teilen von Speicher kommunizieren, sondern durch Kommunikation Speicher teilen". SELECT unterstützt in Kombination mit Channel die Funktion, Kanäle zu verarbeiten, sobald Daten verfügbar sind. Darüber hinaus ermöglicht sync.WaitGroup ein einfaches Warten, bis alle Goroutinen abgeschlossen sind, wodurch der Arbeitsablauf leicht verwaltet werden kann. Dank dieser Tools können Datenkonflikte zwischen Threads vermieden und Concurrency sicherer bearbeitet werden.
Darüber hinaus kann durch die Verwendung von Context auf Benutzerebene (User-Level) die Lebensdauer, Abbruch, Timeout, Deadline und der Anforderungsbereich gesteuert werden, was ein gewisses Maß an Stabilität gewährleistet.
Parallele Ausführung von Goroutine (GOMAXPROCS)
Obwohl die Concurrency von Goroutinen gut ist, stellt sich die Frage, ob sie keine Parallelität unterstützen. Die Anzahl der CPU-Cores ist in letzter Zeit im Gegensatz zur Vergangenheit zweistellig geworden, und auch Heim-PCs haben eine beträchtliche Anzahl von Cores.
Goroutine führt jedoch auch parallele Aufgaben aus, und das ist GOMAXPROCS.
Wenn GOMAXPROCS nicht eingestellt ist, wird es je nach Version unterschiedlich konfiguriert.
Vor 1.5: Standardwert 1. Wenn mehr als 1 erforderlich ist, war die Einstellung mittels
runtime.GOMAXPOCS(runtime.NumCPU())oder ähnlichem zwingend erforderlich.1.5 ~ 1.24: Wurde auf die Anzahl aller verfügbaren logischen Cores geändert. Ab diesem Zeitpunkt mussten Entwickler es nicht mehr einstellen, es sei denn, es gab einen speziellen Bedarf.
1.25: Als beliebte Sprache in Container-Umgebungen überprüft sie cGroup unter Linux und ermittelt die für den Container eingestellte
CPU-Begrenzung.Wenn die Anzahl der logischen Cores 10 beträgt und der CPU-Grenzwert 5 ist, wird
GOMAXPROCSauf den niedrigeren Wert 5 gesetzt.
Die Änderung in 1.25 ist eine sehr bedeutende Anpassung. Dies liegt daran, dass die Sprachnutzung in Container-Umgebungen zugenommen hat. Dadurch konnten unnötige Thread-Erzeugung und Kontextwechsel reduziert werden, wodurch CPU-Throttling verhindert werden kann.
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 wurden abgeschlossen.") // Alle Aufgaben wurden abgeschlossen.
31
32}
33
Goroutine Scheduler (M:N Modell)
Im vorherigen Abschnitt, in dem die Zuweisung und Verwaltung von OS-Threads mittels des M:N-Modells beschrieben wurde, möchte ich nun genauer auf das Goroutine-GMP-Modell eingehen.
- G (Goroutine): Die kleinste Arbeitseinheit, die in Go ausgeführt wird.
- M (Machine): OS Thread (der eigentliche Ausführungsort).
- P (Processor): Ein logischer Prozess, der von der Go-Laufzeit verwaltet wird.
P verfügt zusätzlich über eine lokale Ausführungswarteschlange (Local Run Queue) und fungiert als Scheduler, der die zugewiesenen Gs den Ms zuweist. Einfach ausgedrückt, ist eine Goroutine
Der Ablauf von GMP ist wie folgt:
- Wenn ein G (Goroutine) erzeugt wird, wird es der lokalen Ausführungswarteschlange von P (Processor) zugewiesen.
- P (Processor) weist das G (Goroutine) in seiner lokalen Ausführungswarteschlange einem M (Machine) zu.
- M (Machine) gibt den Zustand des G (Goroutine) zurück: block, complete, preempted.
- Work-Stealing: Wenn die lokale Ausführungswarteschlange eines P leer wird, überprüft ein anderes P die globale Warteschlange. Wenn dort auch kein G (Goroutine) vorhanden ist, stiehlt es Arbeit von einem anderen lokalen P (Processor), um sicherzustellen, dass alle Ms ununterbrochen arbeiten.
- Systemaufrufbehandlung (Blocking): Wenn ein G (Goroutine) während der Ausführung blockiert wird, gerät das M (Machine) in einen Wartezustand. In diesem Fall trennt sich P (Processor) von dem blockierten M (Machine) und verbindet sich mit einem anderen M (Machine), um die nächste G (Goroutine) auszuführen. Dabei gibt es auch während der Wartezeit bei I/O-Operationen keine CPU-Verschwendung.
- Wenn ein G (Goroutine) über einen längeren Zeitraum präemptiert (preempted) wird, erhält ein anderes G (Goroutine) die Möglichkeit zur Ausführung.
Golang führt auch GC (Garbage Collector) auf Goroutinen aus, wodurch der Speicher parallel bereinigt werden kann, mit minimalen Unterbrechungen der Anwendungsausführung (STW), was die Systemressourcen effizient nutzt.
Zusammenfassend ist Golang einer der größten Vorteile der Sprache, und es gibt noch viele weitere. Ich hoffe, dass viele Entwickler Freude an Go haben werden.
Vielen Dank.