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.
In unserem vorherigen Beitrag haben wir das Generator-Parallelitätsmuster untersucht, die Bausteine der anderen Parallelitätsmuster von Go. Hier können Sie es nachlesen:
Schauen wir uns nun an, wie diese Grundelemente zusammenwirken, um leistungsstarke Muster zu bilden, die reale Probleme lösen.
In diesem Beitrag werden wir Pipeline-Muster behandeln und versuchen, sie zu visualisieren. Bereiten wir uns also vor, denn wir werden den gesamten Prozess begleiten.
Eine Pipeline ist wie ein Fließband in einer Fabrik, in der jede Stufe eine bestimmte Aufgabe an den Daten ausführt und das Ergebnis an die nächste Stufe weitergibt.
Wir bauen Pipelines auf, indem wir Goroutinen mit Kanälen verbinden, wobei jede Goroutine eine Stufe darstellt, die Daten empfängt, verarbeitet und an die nächste Stufe sendet.
Lassen Sie uns eine einfache Pipeline implementieren, die:
// Stage 1: Generate numbers func generate(nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out } // Stage 2: Square numbers func square(in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { out <- n * n } }() return out } // Stage 3: Print numbers func print(in <-chan int) { for n := range in { fmt.Printf("%d ", n) } fmt.Println() } func main() { // Connect the pipeline numbers := generate(2, 3, 4) // Stage 1 squares := square(numbers) // Stage 2 print(squares) // Stage 3 }
✏️ Schnelles Byte
<-chan int bezeichnet einen reinen Empfangskanal.
Ein Kanal vom Typ <-chan int kann nur zum Empfangen von Werten verwendet werden, nicht zum Senden. Dies ist nützlich, um strengere Kommunikationsmuster durchzusetzen und versehentliche Schreibvorgänge auf dem Kanal durch den Empfänger zu verhindern.chan int Dies bezeichnet einen bidirektionalen Kanal.
Ein Kanal vom Typ chan int kann sowohl zum Senden als auch zum Empfangen von Werten verwendet werden.
Lassen Sie uns das obige Beispiel visualisieren:
Hier können Sie sehen, dass alle Bausteine der Pipeline Goroutinen sind, die dem Generatormuster folgen. Dies bedeutet, dass der nächste Schritt in der Pipeline im Gegensatz zur sequenziellen Verarbeitung mit der Verarbeitung beginnen kann, sobald die Daten in einem beliebigen Schritt bereit sind.
Grundprinzipien sollten sein:
Lassen Sie uns unseren Code mit einer ordnungsgemäßen Fehlerbehandlung aktualisieren.
type Result struct { Value int Err error } func generateWithError(nums ...int) <-chan Result { out := make(chan Result) go func() { defer close(out) for _, n := range nums { if n < 0 { out <- Result{Err: fmt.Errorf("negative number: %d", n)} return } out <- Result{Value: n} } }() return out } func squareWithError(in <-chan Result) <-chan Result { out := make(chan Result) go func() { defer close(out) for r := range in { if r.Err != nil { out <- r // Forward the error continue } out <- Result{Value: r.Value * r.Value} } }() return out } func main() { // Using pipeline with error handling for result := range squareWithError(generateWithError(2, -3, 4)) { if result.Err != nil { fmt.Printf("Error: %v\n", result.Err) continue } fmt.Printf("Result: %d\n", result.Value) } }
Nehmen wir zum besseren Verständnis ein Beispiel: Wir haben einen Datenverarbeitungsworkflow, der dem unten gezeigten Pipeline-Muster folgt.
? Jede Stufe kann unabhängig entwickelt, getestet und geändert werden
? Änderungen an den Interna einer Stufe wirken sich nicht auf andere Stufen aus
? Einfaches Hinzufügen neuer Stufen oder Ändern vorhandener Stufen
? Klare Trennung der Belange
Und das Beste daran? Wir können mehrere Instanzen jeder Stufe (Worker) für mehr gleichzeitige Anforderungen ausführen, etwa so:
?? Hey, aber ist das nicht das Fan-In- und Fan-Out-Parallelitätsmuster?
Bingo! Guter Fang genau dort. Es handelt sich in der Tat um ein Fan-Out-, Fan-In-Muster, bei dem es sich um eine spezielle Art von Pipeline-Muster handelt. Wir werden im nächsten Beitrag ausführlicher darauf eingehen, also keine Sorge ;)
Bilder in einer Pipeline verarbeiten
// Stage 1: Generate numbers func generate(nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { out <- n } }() return out } // Stage 2: Square numbers func square(in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { out <- n * n } }() return out } // Stage 3: Print numbers func print(in <-chan int) { for n := range in { fmt.Printf("%d ", n) } fmt.Println() } func main() { // Connect the pipeline numbers := generate(2, 3, 4) // Stage 1 squares := square(numbers) // Stage 2 print(squares) // Stage 3 }
oder etwas so Kompliziertes wie die Protokollverarbeitungspipeline
Dieses Muster ist ideal für CPU-gebundene Vorgänge, bei denen die Arbeit unabhängig verarbeitet werden kann. Die Pipeline verteilt die Arbeit auf mehrere Worker und kombiniert die Ergebnisse dann neu. Dies ist besonders effektiv, wenn:
Dieses Muster hilft bei der Bewältigung von Geschwindigkeitsunterschieden zwischen Pipeline-Stufen. Der Puffer fungiert als Stoßdämpfer und ermöglicht es schnellen Stufen, vorwärts zu arbeiten, ohne von langsameren Stufen blockiert zu werden. Dies ist nützlich, wenn:
Dieses Muster optimiert E/A-gebundene Vorgänge, indem es mehrere Elemente in einem einzigen Stapel gruppiert. Anstatt Elemente einzeln zu verarbeiten, werden sie in Gruppen zusammengefasst und gemeinsam verarbeitet. Dies ist wirksam, wenn:
Jedes dieser Muster kann nach Bedarf kombiniert werden. Beispielsweise können Sie eine Stapelverarbeitung mit horizontaler Skalierung verwenden, bei der jeweils mehrere Mitarbeiter Stapel von Artikeln verarbeiten. Der Schlüssel liegt darin, Ihre Engpässe zu verstehen und das geeignete Muster zu wählen, um sie zu beheben.
Das schließt unseren tiefen Einblick in das Generatormuster ab! Als nächstes untersuchen wir das Pipeline-Parallelitätsmuster, in dem wir sehen, wie wir unsere Generatoren miteinander verketten, um leistungsstarke Datenverarbeitungsflüsse zu erstellen.
Wenn Sie diesen Beitrag hilfreich fanden, Fragen haben oder Ihre eigenen Erfahrungen mit Generatoren teilen möchten, würde ich gerne in den Kommentaren unten von Ihnen hören. Ihre Erkenntnisse und Fragen tragen dazu bei, diese Erklärungen für alle noch besser zu machen.
Wenn Sie den visuellen Leitfaden zu Golangs Goroutine und Kanälen verpasst haben, schauen Sie sich ihn hier an:
Bleiben Sie dran für weitere Go-Parallelitätsmuster! ?
Das obige ist der detaillierte Inhalt vonPipeline-Parallelitätsmuster in Go: Ein umfassender visueller Leitfaden. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!