1. Führen Sie jedes Beispiel aus: Lesen Sie nicht nur den Code. Geben Sie es ein, führen Sie es aus und beobachten Sie das Verhalten.⚠️ Wie geht man bei dieser Serie vor?
2. Experimentieren Sie und machen Sie Dinge kaputt: Entfernen Sie Schlafphasen und sehen Sie, was passiert, ändern Sie die Kanalpuffergrößen, ändern Sie die Anzahl der Goroutinen.
Wenn man Dinge kaputt macht, lernt man, wie sie funktionieren
3. Grund für das Verhalten: Bevor Sie geänderten Code ausführen, versuchen Sie, das Ergebnis vorherzusagen. Wenn Sie unerwartetes Verhalten bemerken, halten Sie inne und überlegen Sie, warum. Stellen Sie die Erklärungen in Frage.
4. Erstellen Sie mentale Modelle: Jede Visualisierung repräsentiert ein Konzept. Versuchen Sie, Ihre eigenen Diagramme für geänderten Code zu zeichnen.
Dies ist Teil 1 unserer Serie „Mastering Go Concurrency“, in der wir Folgendes behandeln:
Wir beginnen mit den Grundlagen und entwickeln schrittweise ein Gespür dafür, wie wir diese effektiv nutzen können.
Es wird etwas lang, eher sehr lang, also macht euch bereit.
Wir stehen Ihnen während des gesamten Prozesses zur Seite.
Beginnen wir mit einem einfachen Programm, das mehrere Dateien herunterlädt.
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
Das Programm dauert insgesamt 6 Sekunden, da jeder 2-sekündige Download abgeschlossen sein muss, bevor der nächste beginnt. Stellen wir uns das vor:
Wir können diese Zeit verkürzen, indem wir unser Programm so ändern, dass es Go-Routinen verwendet:
Hinweis: Schlüsselwort vor Funktionsaufruf eingeben
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
Warte was? wurde nichts gedruckt? Warum?
Lassen Sie uns dies visualisieren, um zu verstehen, was passieren könnte.
Aus der obigen Visualisierung gehen wir hervor, dass die Hauptfunktion existiert, bevor die Goroutinen fertig sind. Eine Beobachtung ist, dass der gesamte Lebenszyklus einer Goroutine von der Hauptfunktion abhängt.
Hinweis: Die Hauptfunktion an sich ist eine Goroutine ;)
Um dies zu beheben, müssen wir die Haupt-Goroutine warten lassen, bis die anderen Goroutinen abgeschlossen sind. Dafür gibt es mehrere Möglichkeiten:
Lassen Sie uns einige Sekunden warten, bis die Go-Routinen abgeschlossen sind.
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
Das Problem dabei ist, dass wir möglicherweise nicht wissen, wie viel Zeit eine Goroutine dauern könnte. In diesem Fall haben wir jeweils eine konstante Zeit, aber in realen Szenarien sind wir uns bewusst, dass die Downloadzeit variiert.
Eine sync.WaitGroup in Go ist ein Parallelitätskontrollmechanismus, der verwendet wird, um darauf zu warten, dass die Ausführung einer Sammlung von Goroutinen abgeschlossen ist.
Hier sehen wir uns das in Aktion an und visualisieren es:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
Lassen Sie uns dies visualisieren und die Funktionsweise von sync.WaitGroup verstehen:
Gegenmechanismus:
Synchronisierungsablauf:
Häufige Fallstricke, die es zu vermeiden gilt
package main
import (
"fmt"
"time"
)
func downloadFile(filename string) {
fmt.Printf("Starting download: %s\n", filename)
// Simulate file download with sleep
time.Sleep(2 * time.Second)
fmt.Printf("Finished download: %s\n", filename)
}
func main() {
fmt.Println("Starting downloads...")
startTime := time.Now() // Record start time
go downloadFile("file1.txt")
go downloadFile("file2.txt")
go downloadFile("file3.txt")
// Wait for goroutines to finish
time.Sleep(3 * time.Second)
elapsedTime := time.Since(startTime)
fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
So haben wir ein gutes Verständnis dafür bekommen, wie die Goroutinen funktionieren. Nein, wie kommunizieren zwei Go-Routinen? Hier kommt der Kanal ins Spiel.
Kanäle in Go sind ein leistungsstarkes Nebenläufigkeitsprimitiv, das für die Kommunikation zwischen Goroutinen verwendet wird. Sie bieten Goroutinen die Möglichkeit, Daten sicher auszutauschen.
Stellen Sie sich Kanäle als Pipes vor: Eine Goroutine kann Daten an einen Kanal senden und eine andere kann sie empfangen.
hier sind einige Eigenschaften:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
Warum führt ch <- „hello“ zu einem Deadlock? Da Kanäle von Natur aus blockieren und wir hier „Hallo“ weitergeben, wird die Haupt-Goroutine blockiert, bis es einen Empfänger gibt, und da es keinen Empfänger gibt, bleibt sie hängen.
Beheben wir dieses Problem, indem wir eine Goroutine hinzufügen
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") // Launch downloads concurrently go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") fmt.Println("All downloads completed!") }
Lass uns das visualisieren:
Diesmal wird die Nachricht von einer anderen Goroutine gesendet, damit die Haupt-Goroutine nicht blockiert wird, während sie an den Kanal gesendet wird, sodass sie zu msg := <-ch verschoben wird, wo sie die Haupt-Goroutine blockiert, bis sie die empfängt Nachricht.
Jetzt verwenden wir den Kanal, um das Problem mit dem Datei-Downloader zu beheben (main wartet nicht, bis andere fertig sind).
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() // Record start time go downloadFile("file1.txt") go downloadFile("file2.txt") go downloadFile("file3.txt") // Wait for goroutines to finish time.Sleep(3 * time.Second) elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
Visualisierung:
Machen wir einen Probelauf, um es besser zu verstehen:
Programmstart:
Hauptgoroutine erstellt Fertigkanal
Startet drei Download-Goroutinen
Jede Goroutine erhält einen Verweis auf denselben Kanal
Ausführung herunterladen:
Kanalschleife:
Schleifenverhalten:
Die Reihenfolge der Fertigstellung spielt keine Rolle!
Beobachtungen:
⭐ Jeder Versand (erledigt <- true) hat genau einen Empfang (<-erledigt)
⭐ Die Hauptgoroutine koordiniert alles durch die Schleife
Wir haben bereits gesehen, wie zwei Goroutinen kommunizieren können. Wann? Die ganze Zeit. Vergessen wir nicht, dass die Hauptfunktion auch eine Goroutine ist.
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
Lassen Sie uns dies visualisieren und einen Probelauf durchführen:
Probelauf:
Programmstart (t=0ms)
Erste Nachricht (t=1ms)
Zweite Nachricht (t=101ms)
Dritte Nachricht (t=201ms)
Kanal schließen (t=301 ms)
Abschluss (t=302–303 ms)
Warum brauchen wir gepufferte Kanäle?
Ungepufferte Kanäle blockieren sowohl den Sender als auch den Empfänger, bis die andere Seite bereit ist. Wenn Hochfrequenzkommunikation erforderlich ist, können ungepufferte Kanäle zu einem Engpass werden, da beide Goroutinen pausieren müssen, um Daten auszutauschen.
Eigenschaften gepufferter Kanäle:
Wir sehen es in Aktion:
package main import ( "fmt" "time" ) func downloadFile(filename string) { fmt.Printf("Starting download: %s\n", filename) // Simulate file download with sleep time.Sleep(2 * time.Second) fmt.Printf("Finished download: %s\n", filename) } func main() { fmt.Println("Starting downloads...") startTime := time.Now() downloadFile("file1.txt") downloadFile("file2.txt") downloadFile("file3.txt") elapsedTime := time.Since(startTime) fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime) }
Ausgabe (vor dem Auskommentieren des ch<-"dritten")
Warum hat es die Haupt-Goroutine nicht blockiert?
Ein gepufferter Kanal ermöglicht das Senden bis zu seiner Kapazität, ohneden Absender zu blockieren.
Der Kanal hat eine Kapazität von 2, was bedeutet, dass er zwei Werte in seinem Puffer speichern kann, bevor er blockiert wird.
Der Puffer ist bereits mit „erster“ und „zweiter“ voll. Da es keinen gleichzeitigen Empfänger gibt, der diese Werte konsumiert, wird der Sendevorgang auf unbestimmte Zeit blockiert.
Da die Haupt-Goroutine auch für das Senden verantwortlich ist und es keine anderen aktiven Goroutinen gibt, die Werte vom Kanal empfangen, gerät das Programm beim Versuch, die dritte Nachricht zu senden, in einen Deadlock.
Das Auskommentieren der dritten Nachricht führt zu einem Deadlock, da die Kapazität jetzt voll ist und die dritte Nachricht blockiert wird, bis der Puffer frei wird.
Aspect | Buffered Channels | Unbuffered Channels |
---|---|---|
Purpose | For decoupling sender and receiver timing. | For immediate synchronization between sender and receiver. |
When to Use | - When the sender can proceed without waiting for receiver. | - When sender and receiver must synchronize directly. |
- When buffering improves performance or throughput. | - When you want to enforce message-handling immediately. | |
Blocking Behavior | Blocks only when buffer is full. | Sender blocks until receiver is ready, and vice versa. |
Performance | Can improve performance by reducing synchronization. | May introduce latency due to synchronization. |
Example Use Cases | - Logging with rate-limited processing. | - Simple signaling between goroutines. |
- Batch processing where messages are queued temporarily. | - Hand-off of data without delay or buffering. | |
Complexity | Requires careful buffer size tuning to avoid overflows. | Simpler to use; no tuning needed. |
Overhead | Higher memory usage due to the buffer. | Lower memory usage; no buffer involved. |
Concurrency Pattern | Asynchronous communication between sender and receiver. | Synchronous communication; tight coupling. |
Error-Prone Scenarios | Deadlocks if buffer size is mismanaged. | Deadlocks if no goroutine is ready to receive or send. |
Verwenden Sie gepufferte Kanäle, wenn:
Verwenden Sie ungepufferte Kanäle, wenn:
Diese Grundlagen schaffen die Grundlage für fortgeschrittenere Konzepte. In unseren kommenden Beiträgen werden wir Folgendes untersuchen:
Nächster Beitrag:
Bleiben Sie dran, während wir unser Verständnis der leistungsstarken Parallelitätsfunktionen von Go weiter ausbauen!
Das obige ist der detaillierte Inhalt vonGoroutinen und Kanäle in Golang mit intuitiven Bildern verstehen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!