Concurrency und Multithreading in Go: Von A bis Z
Go (Golang) wurde mit Blick auf moderne Rechenlasten entwickelt. Im Zeitalter von Mehrkernprozessoren und Cloud-Diensten ist die Fähigkeit, effizient mit Concurrency zu arbeiten, zu einer unverzichtbaren Fähigkeit für Backend-Entwickler geworden. In diesem Artikel werden wir untersuchen, wie Go die parallele Ausführung von Code bewältigt, wie sich Goroutinen von OS-Threads unterscheiden und wie man Channels und Mutexe richtig einsetzt.
1. Goroutinen: Leichtgewichtige Threads
Eine Goroutine ist eine Funktion oder Methode, die gleichzeitig mit anderen Goroutinen ausgeführt wird. Im Gegensatz zu Betriebssystem-Threads haben Goroutinen minimale Gemeinkosten: Ihr Stack beginnt bei 2-4 KB und kann dynamisch wachsen. Eine Goroutine zu starten ist unglaublich einfach – fügen Sie einfach das Schlüsselwort go vor dem Funktionsaufruf hinzu.
package main
import ( "fmt" "time")
func say(text string) { for i := 0; i < 3; i++ { fmt.Println(text) time.Sleep(100 * time.Millisecond) }}
func main() { go say("Hallo von der Goroutine") say("Hallo von main") // Geben Sie der Goroutine Zeit, abzuschließen time.Sleep(500 * time.Millisecond)}Beachten Sie: Das Programm wird sofort nach der Ausführung der Funktion main() beendet, ohne auf den Abschluss der gestarteten Goroutinen zu warten. Zur Synchronisation verwenden wir time.Sleep (in realen Projekten sync.WaitGroup oder Channels).
2. Channels: Sichere Datenübertragung
Channels sind typisierte "Röhren", über die Goroutinen Daten austauschen. Sie lösen zwei zentrale Aufgaben: die Synchronisation der Ausführung und die Übergabe von Werten. Channels können gepuffert oder ungepuffert sein.
Ungepufferter Channel
Der Sende-Vorgang blockiert die Goroutine, bis eine andere Goroutine einen Lesevorgang durchführt. Dies gewährleistet eine Synchronisation "im laufenden Betrieb".
package main
import "fmt"
func main() { ch := make(chan string)
go func() { ch <- "Hallo von der Goroutine!" }()
msg := <-ch fmt.Println(msg)}Gepufferter Channel
Ein Channel mit Puffer ermöglicht das Senden mehrerer Werte ohne sofortiges Lesen, bis der Puffer voll ist.
package main
import "fmt"
func main() { ch := make(chan int, 2) ch <- 10 ch <- 20 // ch <- 30 // deadlock! Puffer ist voll
fmt.Println(<-ch) // 10 fmt.Println(<-ch) // 20}Range und close
Das Schließen eines Channels signalisiert den Empfängern, dass keine weiteren Daten mehr kommen. Die for range-Schleife wird automatisch beendet, wenn der Channel geschlossen wird.
package main
import "fmt"
func producer(ch chan int) { for i := 1; i <= 5; i++ { ch <- i } close(ch)}
func main() { ch := make(chan int) go producer(ch)
for val := range ch { fmt.Println("Erhalten:", val) }}3. Synchronisation und Mutexe
Trotz der Leistungsfähigkeit von Channels ist manchmal ein direkter Zugriff auf gemeinsame Daten erforderlich. Das Paket sync bietet Mutexe (sync.Mutex) und Reader-Writer-Mutexe (sync.RWMutex) zum Schutz kritischer Abschnitte.
package main
import ( "fmt" "sync")
var counter = 0var mu sync.Mutex
func increment(wg *sync.WaitGroup) { defer wg.Done() mu.Lock() counter++ mu.Unlock()}
func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go increment(&wg) } wg.Wait() fmt.Println("Endgültiger Zählerstand:", counter)}Hier wartet sync.WaitGroup auf den Abschluss aller Goroutinen, und der Mutex verhindert einen Datenwettlauf (data race). Ohne den Mutex wäre das Ergebnis unvorhersehbar.