Middle+
Erzählen Sie von der Planung von Goroutinen in Go.
sobes.tech KI
Antwort von AI
Der Scheduler für Goroutines in Go verwendet das M:N-Modell, bei dem M Goroutines auf N Betriebssystem-Threads abgebildet werden. Es wechselt effizient zwischen Goroutines auf Threads und implementiert Multitasking ohne Preemption mit Haltepunkten (Preemption Points).
Hauptkomponenten des GPM-Modells:
- G (Goroutine): Abstraktion der auszuführenden Codeeinheit, leichter Thread, verwaltet vom Go-Scheduler.
- P (Prozessor): Logischer Prozessor, der einen Kontext einer lokalen Warteschlange von Goroutines und Ressourcen wie Mutexes und Scheduler repräsentiert. Die Anzahl der P wird standardmäßig durch die Anzahl der logischen Kerne des Prozessors bestimmt, kann aber durch
GOMAXPROCSgeändert werden. - M (Machine/Thread): Betriebssystem-Thread, der Code ausführt. M ist mit P verbunden und führt Goroutines aus der lokalen P-Warteschlange oder der globalen Warteschlange aus.
Funktionsweise:
- Eine neue Goroutine wird erstellt und in die lokale Warteschlange eines verfügbaren P gelegt.
- M, das mit P verbunden ist, nimmt eine Goroutine aus der lokalen Warteschlange und führt sie aus.
- Wenn die lokale Warteschlange leer ist, versucht M, Goroutines von anderen P zu stehlen.
- Wenn P blockiert (z.B. bei einem Systemaufruf), trennt sich das zugehörige M ab, und ein anderes M verbindet sich mit diesem P. Das blockierte M kann wiederverwendet werden, wenn der Systemaufruf beendet ist.
- Wenn eine Goroutine auf einem Kanal oder Mutex blockiert, wird sie in die Warteschlange für Wartende gestellt, und der Scheduler wählt eine andere Goroutine zur Ausführung auf demselben M.
- Der Scheduler führt periodische Überprüfungen durch, um lang laufende Goroutines zu verdrängen, sodass andere Goroutines CPU-Zeit erhalten.
Preemption Points treten an bestimmten Stellen auf:
- Funktions- und Methodenaus calls.
- Schleifen, die Funktionsaufrufe enthalten.
- Speicherzuweisungen.
- Synchronisationsprimitive (Kanäle, Mutexes).
Beispiel für Scheduling:
package main
import (
"fmt"
"runtime"
"time"
)
func worker(id int) {
fmt.Printf("Goroutine %d beginnt zu arbeiten\n", id)
time.Sleep(time.Second) // Arbeit simulieren
fmt.Printf("Goroutine %d hat die Arbeit beendet\n", id)
}
func main() {
runtime.GOMAXPROCS(2) // Anzahl der logischen Prozessoren festlegen
for i := 1; i <= 5; i++ {
go worker(i) // Goroutines erstellen
}
time.Sleep(3 * time.Second) // Warten auf Abschluss der Goroutines
fmt.Println("Haupt-Goroutine beendet")
}
In diesem Beispiel werden 5 Goroutines auf 2 logische Prozessoren geplant. Der Go-Scheduler verteilt diese Goroutines auf die verfügbaren Betriebssystem-Threads, nutzt lokale und möglicherweise globale Warteschlangen sowie den "Work Stealing"-Mechanismus.
Vorteile dieses Ansatzes:
- Skalierbarkeit: Effiziente Nutzung von Mehrkernsystemen.
- Geringer Overhead: Leichte Goroutines im Vergleich zu OS-Threads.
- Benutzerfreundlichkeit: Die integrierte Planung im Sprachkern macht parallele Programmierung zugänglicher.