Parallelität in Go: Goroutinen, Kanäle und async/await (Vergleich)

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

Warum Go alle in der Parallelität überholt hat?

\n\n

Stellen Sie sich einen Chefkoch vor, der zehn Gerichte gleichzeitig zubereitet. Er rennt nicht wie wild von Herd zu Herd – er wechselt effizient zwischen den Aufgaben: Während das Fleisch schmort, schneidet er den Salat; während das Wasser kocht, serviert er den Teller. In der Programmierwelt nennt man das Parallelität – die Fähigkeit, mehrere Aufgaben zu verwalten, ohne auf das Ende jeder einzelnen zu warten.

\n\n

Die Sprache Go (Golang) wurde genau zur Lösung von Parallelitätsproblemen entwickelt. Ihre Schöpfer bei Google hatten genug von den Komplexitäten der Multithreading-Programmierung in C++ und Java. Sie erfanden Goroutinen – leichtgewichtige Threads, die mit einer einzigen Codezeile gestartet werden. In diesem Artikel analysieren wir, wie Parallelität in Go funktioniert, vergleichen sie mit OS-Threads und der async/await-Paradigma aus Python, und zeigen vor allem reale Codebeispiele, die Sie noch heute anwenden können.

\n\n

Goroutinen vs. OS-Threads: Schlacht der leichten Kavallerie

\n\n

Wenn ein Entwickler „Multithreading“ hört, stellt er sich normalerweise Betriebssystem-Threads vor. Aber in Go ist alles anders. Lassen Sie uns die wichtigsten Unterschiede analysieren.

\n\n

Was ist ein OS-Thread?

\n

Ein Thread ist ein schweres Objekt, das vom OS-Kernel verwaltet wird. Jeder Thread hat seinen eigenen Stack (normalerweise 1-8 MB), erfordert Systemaufrufe zum Erstellen und Umschalten. 10.000 Threads zu erstellen bedeutet, Speicher und CPU zu töten. In Go steckt eine andere Philosophie.

\n\n

Goroutine – der leichte Thread von Go

\n

Eine Goroutine ist eine Funktion, die parallel ausgeführt wird. Sie wird mit dem Schlüsselwort go gestartet. Die Hauptmerkmale:

\n
    \n
  • Stack-Größe: Beginnt mit 2-4 KB und wächst/schrumpft dynamisch.
  • \n
  • Erstellung: Tausende von Goroutinen werden in Millisekunden erstellt.
  • \n
  • Scheduler: Die Go-Runtime verteilt Goroutinen selbstständig auf OS-Threads (normalerweise entspricht ihre Anzahl der Anzahl der CPU-Kerne).
  • \n
  • Syntax: Fügen Sie einfach go vor dem Funktionsaufruf hinzu.
  • \n
\n\n
package main\n\nimport (\n    "fmt"\n    "time"\n)\n\nfunc sayHello(name string) {\n    for i := 0; i < 3; i++ {\n        fmt.Println("Hallo,", name)\n        time.Sleep(100 * time.Millisecond)\n    }\n}\n\nfunc main() {\n    // Starten zwei Goroutinen\n    go sayHello("Go")\n    go sayHello("Goroutine")\n\n    // Geben den Goroutinen Zeit zur Ausführung\n    time.Sleep(1 * time.Second)\n    fmt.Println("Hauptfunktion beendet")\n}
\n\n

Ergebnis: Die Nachrichten der beiden Goroutinen werden gemischt. Go wechselt automatisch zwischen ihnen, ohne sich gegenseitig zu blockieren. Versuchen Sie, diesen Code auszuführen – Sie werden sehen, wie „Hallo, Go“ und „Hallo, Goroutine“ abwechselnd ausgegeben werden.

\n\n

Kanäle: Das wichtigste Synchronisationswerkzeug

\n\n

Goroutinen sind großartig, aber wie kommunizieren sie? In Go gibt es standardmäßig keinen gemeinsamen Speicher. Stattdessen wird das Prinzip verwendet: „Kommunizieren Sie nicht, indem Sie Speicher teilen; teilen Sie Speicher, indem Sie kommunizieren“. Dazu dienen Kanäle (channels).

\n\n

Ein Kanal ist eine typisierte „Pipeline“, über die Goroutinen Daten senden und empfangen. Das ist sicher, einfach und elegant.

\n\n

Beispiel: Parallele Zahlenverarbeitung

\n
package main\n\nimport "fmt"\n\nfunc squareWorker(numbers <-chan int, results chan<- int) {\n    for n := range numbers {\n        results <- n * n\n    }\n    close(results)\n}\n\nfunc main() {\n    numbers := make(chan int, 5)\n    results := make(chan int, 5)\n\n    // Starten einen Worker\n    go squareWorker(numbers, results)\n\n    // Senden Daten\n    numbers <- 2\n    numbers <- 4\n    numbers <- 6\n    close(numbers)\n\n    // Lesen Ergebnisse\n    for res := range results {\n        fmt.Println("Quadrat:", res)\n    }\n}
\n\n

In diesem Beispiel verwenden wir gepufferte Kanäle (das zweite Argument ist die Puffergröße). Die Worker-Goroutine liest aus dem Kanal numbers, quadriert die Werte und schreibt sie in den Kanal results. Die Hauptfunktion liest die Ergebnisse aus und gibt sie aus.

Blogs

Buchempfehlungen