Parallelität und Multithreading in Go: Fortgeschrittenes Niveau
Go (Golang) wurde von Anfang an als Sprache für die Entwicklung skalierbarer und leistungsfähiger Anwendungen konzipiert. Grundlage dafür ist ein leistungsstarkes Parallelitätsmodell, das auf Goroutinen und Kanälen basiert. Wenn Sie bereits die grundlegenden Beispiele mit go func() und einfachen Kanälen gemeistert haben, ist es an der Zeit, zur nächsten Stufe überzugehen. In diesem Artikel werden wir fortgeschrittene Muster für die Arbeit mit Parallelität in Go behandeln, die Ihnen helfen, zuverlässigen, effizienten und sicheren Code zu schreiben.
1. Verwaltung des Lebenszyklus von Goroutinen: Context und WaitGroup
Eines der häufigsten Probleme ist das Auslaufen von Goroutinen. Eine Goroutine, die unendlich auf Daten aus einem Kanal wartet, wird niemals beendet, wenn der Kanal nicht richtig geschlossen wird. Für die Verwaltung des Lebenszyklus von Goroutinen gibt es in Go zwei Hauptwerkzeuge: sync.WaitGroup und context.Context.
sync.WaitGroup zum Warten auf Beendigung
Verwenden Sie sync.WaitGroup, wenn Sie auf die Beendigung einer festgelegten Gruppe von Goroutinen warten müssen. Dies ist ein einfacher und effektiver Mechanismus.
package main
import (\t"fmt"\t"sync"\t"time")
func worker(id int, wg *sync.WaitGroup) {\tdefer wg.Done() // Zähler beim Verlassen verringern\tfmt.Printf("Worker %d hat die Arbeit begonnen", id)\ttime.Sleep(time.Second)\tfmt.Printf("Worker %d hat die Arbeit beendet", id)}
func main() {\tvar wg sync.WaitGroup
\tfor i := 1; i <= 3; i++ {\t\twg.Add(1) // Zähler vor dem Start erhöhen\t\tgo worker(i, &wg)\t}
\twg.Wait() // Blockieren, bis der Zähler 0 ist\tfmt.Println("Alle Worker wurden beendet")}context.Context für Abbruch und Fristen
Wenn eine Goroutine in einer Schleife läuft oder eine langwierige Operation ausführt, muss sie in der Lage sein, sich auf ein externes Signal hin korrekt zu beenden. context.Context ist der Standardweg, um ein Signal für Abbruch, Frist oder Timeout zu übermitteln.
package main
import (\t"context"\t"fmt"\t"time")
func longRunningTask(ctx context.Context) {\tfor {\t\tselect {\t\tcase <-ctx.Done():\t\t\tfmt.Println("Goroutine hat Stoppsignal erhalten")\t\t\treturn\t\tdefault:\t\t\ttime.Sleep(500 * time.Millisecond)\t\t\tfmt.Println("Arbeite...")\t\t}\t}}
func main() {\tctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)\tdefer cancel() // Wichtig: cancel aufrufen, um Ressourcen freizugeben
\tgo longRunningTask(ctx)
\ttime.Sleep(3 * time.Second)\tfmt.Println("Main wurde beendet")}2. Kanäle: Pufferung, Schließen und das Fan-Out/Fan-In-Muster
Kanäle sind die primäre Methode zur Synchronisation und Datenübertragung zwischen Goroutinen. Auf fortgeschrittenem Niveau ist es wichtig, den Unterschied zwischen gepufferten und ungepufferten Kanälen zu verstehen, sie richtig zu schließen und Aggregationsmuster zu verwenden.
Gepuffert vs. Ungepuffert
Ein ungepufferter Kanal (ohne Puffer) blockiert den Sender, bis der Empfänger die Daten gelesen hat. Ein gepufferter Kanal ermöglicht das Senden mehrerer Nachrichten ohne Wartezeit, bis der Puffer voll ist. Die Wahl hängt von der Aufgabe ab: Für strenge Synchronisation verwenden Sie ungepufferte, zum Glätten der Last gepufferte Kanäle.
Fan-Out/Fan-In-Muster
Dies ist eines der leistungsstärksten Muster. Fan-Out – Verteilung von Aufgaben auf mehrere Goroutinen (Worker). Fan-In – Sammeln von Ergebnissen mehrerer Goroutinen in einem Kanal.
package main
import (\t"fmt"\t"sync")
func producer(nums ...int) <-chan int {\tout := make(chan int)\tgo func() {\t\tfor _, n := range nums {\t\t\tout <- n\t\t}\t\tclose(out)\t}()\treturn out}
func square(in <-chan int) <-chan int {\tout := make(chan int)\tgo func() {\t\tfor n := range in {\t\t\tout <- n * n\t\t}\t\tclose(out)\t}()\treturn out}
func merge(channels ...<-chan int) <-chan int {\t