Einführung in die Nebenläufigkeit in Go
Die Sprache Go (Golang) wurde bei Google unter Berücksichtigung moderner Anforderungen an Leistung und Skalierbarkeit entwickelt. Eine der wichtigsten Eigenschaften von Go ist die integrierte Unterstützung für Nebenläufigkeit (concurrency) – die Fähigkeit eines Programms, mehrere Aufgaben gleichzeitig auszuführen. Im Gegensatz zur traditionellen Multithreading, bei dem Threads vom Betriebssystem verwaltet werden, bietet Go leichtgewichtige Goroutinen und praktische Synchronisationsprimitive – Kanäle.
In diesem Artikel behandeln wir:
- Was Goroutinen sind und wie man sie startet
- Kanäle für den Datenaustausch zwischen Goroutinen
- Mutexe für den sicheren Zugriff auf gemeinsame Ressourcen
- Praktische Beispiele und Muster für nebenläufigen Code
1. Goroutinen: Leichtgewichtige Threads in Go
Eine Goroutine ist eine Funktion, die nebenläufig zu anderen Goroutinen ausgeführt wird. Sie belegt nur wenige Kilobyte Stack (im Gegensatz zu OS-Threads, deren Stack megabytegroß sein kann). Eine Goroutine startet man einfach durch das Hinzufügen des Schlüsselworts go vor dem Funktionsaufruf.
Beispiel: Starten von Goroutinen
package main
import ( "fmt" "time")
func sayHello(name string) { for i := 0; i < 3; i++ { fmt.Printf("Hallo, %s! (Iteration %d)", name, i) time.Sleep(100 * time.Millisecond) }}
func main() { go sayHello("Alice") // Goroutine starten go sayHello("Bob") // Noch eine Goroutine time.Sleep(1 * time.Second) // Auf Beendigung warten fmt.Println("Hauptfunktion beendet")}Wichtig: Das Programm wird beendet, wenn die Funktion main endet, auch wenn andere Goroutinen noch laufen. Deshalb verwenden wir time.Sleep zum Warten. In echtem Code sollte man besser sync.WaitGroup verwenden.
Verwendung von sync.WaitGroup
package main
import ( "fmt" "sync")
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // Zähler bei Beendigung verringern fmt.Printf("Arbeiter %d hat mit der Arbeit begonnen", id) // ... irgendeine Arbeit ... fmt.Printf("Arbeiter %d hat die Arbeit beendet", id)}
func main() { var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) // Zähler vor dem Start der Goroutine erhöhen go worker(i, &wg) }
wg.Wait() // main blockieren, bis alle Goroutinen beendet sind fmt.Println("Alle Arbeiter haben ihre Arbeit beendet")}2. Kanäle: Datenaustausch zwischen Goroutinen
Kanäle (channels) sind typisierte "Rohre", durch die Goroutinen Werte senden und empfangen können. Kanäle ermöglichen eine sichere Synchronisation ohne explizite Sperren.
Erstellen und Verwenden von Kanälen
package main
import "fmt"
func main() { // Kanal für ganze Zahlen erstellen ch := make(chan int)
// Goroutine starten, die Daten sendet go func() { ch <- 42 // Wert in den Kanal senden }()
// Wert aus dem Kanal empfangen value := <-ch fmt.Println("Empfangen:", value)}Standardmäßig sind Kanäle ungepuffert: Das Senden blockiert, bis der Empfänger bereit ist, die Daten zu empfangen, und umgekehrt. Dies gewährleistet die Synchronisation.
Gepufferte Kanäle
package main
import "fmt"
func main() { // Kanal mit einem Puffer für 3 Elemente ch := make(chan string, 3)
// Senden ohne Blockierung (solange der Puffer nicht voll ist) ch <- "eins" ch <- "zwei" ch <- "drei"
// Lesen fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)}Schließen von Kanälen und range
package main
import "fmt"
func fibonacci(n int, ch chan int) { a, b := 0, 1 for i := 0; i < n; i++ { ch <-