Warum Go alle in der Parallelität überholt hat?
\n\nStellen 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\nDie 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\nGoroutinen vs. OS-Threads: Schlacht der leichten Kavallerie
\n\nWenn 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\nWas ist ein OS-Thread?
\nEin 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\nGoroutine – der leichte Thread von Go
\nEine Goroutine ist eine Funktion, die parallel ausgeführt wird. Sie wird mit dem Schlüsselwort go gestartet. Die Hauptmerkmale:
- \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
govor dem Funktionsaufruf hinzu. \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\nErgebnis: 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\nKanäle: Das wichtigste Synchronisationswerkzeug
\n\nGoroutinen 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\nEin Kanal ist eine typisierte „Pipeline“, über die Goroutinen Daten senden und empfangen. Das ist sicher, einfach und elegant.
\n\nBeispiel: Parallele Zahlenverarbeitung
\npackage 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\nIn 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.