Parallelität und Multithreading in Go: Goroutinen und Kanäle

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

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 <-

Blogs

Buchempfehlungen