Gepufferte vs. ungepufferte Channels
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 61 von 107.
Die bisher von uns verwendeten Kanäle sind ungepuffert. Sie haben keine Kapazität, um Werte zu speichern. Eine Sendeoperation blockiert, bis eine andere Goroutine empfängt, und umgekehrt. Gepufferte Kanäle fügen einen internen Speicher hinzu, sodass Sendevorgänge ohne einen sofortigen Empfänger abgeschlossen werden können.
Erstelle einen gepufferten Kanal, indem du eine Kapazität an make übergibst:
// Ungepuffert - Kapazität 0
unbuffered := make(chan int)
// Gepuffert - Kapazität 3
buffered := make(chan int, 3)Bei einem gepufferten Kanal blockieren Sendevorgänge nur, wenn der Puffer voll ist, und Empfangsvorgänge nur, wenn der Puffer leer ist:
ch := make(chan string, 2)
ch <- "first" // blockiert nicht - Puffer hat Platz
ch <- "second" // blockiert nicht - Puffer hat Platz
// ch <- "third" würde blockieren - Puffer ist voll
fmt.Println(<-ch) // "first"
fmt.Println(<-ch) // "second"Verwende len(ch), um zu überprüfen, wie viele Elemente sich derzeit im Puffer befinden, und cap(ch) für die Gesamtkapazität.
Wann welches verwendet wird: Ungepufferte Kanäle ermöglichen eine enge Synchronisierung: Der Sender weiß, dass der Empfänger den Wert erhalten hat. Gepufferte Kanäle entkoppeln den zeitlichen Ablauf von Sender und Empfänger und sind nützlich, wenn Produzenten und Konsumenten mit unterschiedlicher Geschwindigkeit arbeiten. Gepufferte Kanäle können jedoch Synchronisierungsfehler verschleiern. Verwende daher bevorzugt ungepufferte Kanäle, es sei denn, du hast einen bestimmten Grund für eine Pufferung.
Aufgabe
EinfachErstellen wir einen Batch-Prozessor, der den Unterschied zwischen gepufferten und ungepufferten Kanälen demonstriert. Du erstellst ein System, in dem ein Produzent Elemente an einen Prozessor sendet, wobei gepufferte Kanäle verwendet werden, um ihre zeitliche Abstimmung zu entkoppeln und dem Produzenten zu ermöglichen, vorauszuarbeiten.
Du organisierst deinen Code über zwei Dateien:
processor.go: Definiere deine Batch-Verarbeitungslogik mithilfe von Kanälen.Erstelle eine
Item-Struktur mit den FeldernID(int) undValue(string).Implementiere zwei Funktionen:
Producer(items []Item, out chan Item)– Sendet jedes Element an den Ausgabekanal. Gib nach dem Senden jedes ElementsProduced item [ID]aus. Nachdem alle Elemente gesendet wurden, gib die aktuelle Anzahl der im Puffer wartenden Elemente mithilfe vonlen(out)im FormatBuffer has [count] itemsaus. Schließe anschließend den Kanal.Consumer(in chan Item) []string– Empfängt Elemente vom Eingangskanal und sammelt sie in einem Slice formatierter Zeichenketten. Für jedes empfangene Element soll die Zeichenkette wie folgt aussehen:Consumed: [ID] - [Value]. Gib den Slice zurück, sobald der Kanal geschlossen ist.
main.go: Richte den gepufferten Kanal ein und koordiniere Produzent und Konsument.Lies zuerst die Pufferkapazität, dann die Anzahl der Elemente und anschließend die ID und den Wert jedes Elements ein. Erstelle einen gepufferten Kanal mit der angegebenen Kapazität. Führe zuerst den Producer aus (nicht als Goroutine), um den Puffer zu füllen, und führe anschließend den Consumer aus, um alle Elemente zu verarbeiten. Gib jedes konsumierte Ergebnis in einer separaten Zeile aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Pufferkapazität (Ganzzahl)
- Zeile 2: Anzahl der Elemente (Ganzzahl)
- Folgende Zeilen: Für jedes Element zwei Zeilen – die Element-ID (Ganzzahl), dann sein Wert (Zeichenkette)
Zum Beispiel bei folgenden Eingaben:
3
3
1
apple
2
banana
3
cherryDeine Ausgabe sollte wie folgt aussehen:
Produced item 1
Produced item 2
Produced item 3
Buffer has 3 items
Consumed: 1 - apple
Consumed: 2 - banana
Consumed: 3 - cherryBeachte, dass der Produzent bei einer Pufferkapazität von 3 alle 3 Elemente senden kann, ohne zu blockieren, und der Puffer 3 wartende Elemente anzeigt, bevor der Konsument startet. Wenn der Puffer kleiner als die Anzahl der Elemente wäre, würde der Produzent auf freien Platz warten und blockieren.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Pufferkapazität lesen
line, _ := reader.ReadString('\n')
bufferCapacity, _ := strconv.Atoi(strings.TrimSpace(line))
// Anzahl der Elemente lesen
line, _ = reader.ReadString('\n')
numItems, _ := strconv.Atoi(strings.TrimSpace(line))
// ID und Wert jedes Elements lesen
items := make([]Item, numItems)
for i := 0; i < numItems; i++ {
line, _ = reader.ReadString('\n')
id, _ := strconv.Atoi(strings.TrimSpace(line))
line, _ = reader.ReadString('\n')
value := strings.TrimSpace(line)
items[i] = Item{ID: id, Value: value}
}
// TODO: Erstelle einen gepufferten Channel mit der angegebenen Kapazität
// TODO: Führe den Producer aus (nicht als Goroutine), um den Puffer zu füllen
// TODO: Führe den Consumer aus, um alle Elemente zu verarbeiten
// TODO: Gib jedes konsumierte Ergebnis in einer separaten Zeile aus
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker PoolÜbe selbstständig: Online-Go-Compiler