Concurrency und Multithreading in Go: Von Goroutinen bis zu Channels
Die Sprache Go (Golang) wurde bei Google unter Berücksichtigung moderner Anforderungen an Leistung und Skalierbarkeit entwickelt. Ihre Hauptstärke ist die integrierte Unterstützung von Concurrency, die auf Sprachebene und nicht über externe Bibliotheken implementiert ist. Im Gegensatz zu Python, wo Multithreading durch den GIL (Global Interpreter Lock) eingeschränkt ist, bietet Go leichtgewichtige Goroutinen, die es ermöglichen, Tausende von Aufgaben gleichzeitig effizient auszuführen. In diesem Artikel werden wir die wichtigsten Konzepte von Concurrency und Multithreading in Go behandeln, einschließlich Goroutinen, Channels, Mutexe und WaitGroups, mit praktischen Codebeispielen.
1. Goroutinen: Leichtgewichtige Threads in Go
Eine Goroutine ist eine Funktion, die gleichzeitig mit anderen Funktionen im selben Adressraum ausgeführt wird. Sie läuft auf System-Threads, nutzt aber den eigenen Scheduler von Go, was sie deutlich günstiger macht als einen normalen OS-Thread. Eine Goroutine wird mit dem Schlüsselwort go erstellt.
package main
import ( "fmt" "time")
func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) }}
func main() { go say("Hello") // Goroutine starten say("World") // Normaler Aufruf im Hauptthread}In diesem Beispiel wird say("Hello") in einer separaten Goroutine ausgeführt, während say("World") im Hauptthread läuft. Die Ausgabe wird gemischt sein, da beide Funktionen gleichzeitig arbeiten. Wichtig zu beachten: Wenn die Hauptfunktion main endet, werden alle Goroutinen zwangsweise gestoppt.
2. Channels: Sichere Datenübertragung zwischen Goroutinen
Channels sind typisierte "Röhren", über die Goroutinen miteinander kommunizieren. Sie sorgen für Synchronisation ohne explizite Verwendung von Mutexen. Channels können gepuffert (mit Kapazität) oder ungepuffert (synchron) sein.
Ungepufferter Channel
package main
import "fmt"
func sum(s []int, c chan int) { total := 0 for _, v := range s { total += v } c <- total // Ergebnis an den Channel senden}
func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int)
go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c)
x, y := <-c, <-c // Vom Channel empfangen
fmt.Println(x, y, x+y)}Hier berechnen zwei Goroutinen die Summen der Array-Hälften und senden die Ergebnisse an den Channel. Die Hauptgoroutine blockiert, bis beide Werte empfangen wurden. Ein ungepufferter Channel fungiert als synchroner Punkt: Das Senden blockiert, bis der Empfänger bereit ist, und umgekehrt.
Gepufferter Channel
package main
import "fmt"
func main() { ch := make(chan int, 2) // Puffer für 2 Elemente ch <- 1 ch <- 2 // ch <- 3 // dies würde einen Deadlock verursachen, da der Puffer voll ist fmt.Println(<-ch) fmt.Println(<-ch)}Ein gepufferter Channel erlaubt das Senden von Daten ohne Blockierung, solange der Puffer nicht voll ist. Dies ist nützlich, wenn die Senderate höher ist als die Verarbeitungsrate.
3. Synchronisation: WaitGroup und Mutexe
Obwohl Channels die meisten Szenarien abdecken, ist manchmal eine feinere Kontrolle über die Synchronisation erforderlich. Dafür bietet das Paket sync die WaitGroup (Warten auf das Ende einer Gruppe von Goroutinen) und Mutex (gegenseitiger Ausschluss zum Schutz gemeinsamer Daten).
WaitGroup: Auf alle Goroutinen warten
package main
import ( "fmt" "sync")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Zähler bei Beendigung verringern fmt.Printf("Worker %d starting", id) // Arbeit simulieren fmt.Printf("Worker %d done", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // Zähler erhöhen