sync.WaitGroup
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 64 von 107.
In der Lektion „Grundlagen der Goroutinen“ haben wir time.Sleep verwendet, um auf das Beenden von Goroutinen zu warten: ein anfälliger Ansatz. sync.WaitGroup bietet eine geeignete Möglichkeit, darauf zu warten, dass eine Sammlung von Goroutinen ihre Arbeit abschließt.
Ein WaitGroup funktioniert wie ein Zähler. Du erhöhst ihn, bevor du eine Goroutine startest, verringerst ihn, wenn die Goroutine beendet ist, und blockierst, bis der Zähler null erreicht:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // Zähler erhöhen
go func(id int) {
defer wg.Done() // verringern wenn fertig
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // blockieren bis Zähler 0 ist
fmt.Println("All workers complete")
}Die drei wichtigsten Methoden sind Add(n), um den Zähler um n zu erhöhen, Done(), um ihn um eins zu verringern (entspricht Add(-1)), und Wait(), um zu blockieren, bis der Zähler null erreicht. Die Verwendung von defer wg.Done() stellt sicher, dass der Zähler auch dann verringert wird, wenn die Goroutine in Panik gerät.
Ein häufiger Fehler besteht darin, Add innerhalb der Goroutine statt davor aufzurufen. Dadurch entsteht eine Race Condition, bei der Wait zurückkehren könnte, bevor alle Goroutinen registriert sind:
// Falsch – Race Condition
go func() {
wg.Add(1) // könnte nach Wait() laufen
defer wg.Done()
}()
// Richtig – Add vor dem Starten
wg.Add(1)
go func() {
defer wg.Done()
}()WaitGroups werden häufig in Strukturen eingebettet, um nebenläufige Vorgänge zu koordinieren, wodurch sie für die Erstellung threadsicherer Typen in Go unverzichtbar sind.
Aufgabe
EinfachErstellen wir einen Download-Manager, der mehrere gleichzeitig ausgeführte Dateidownloads mithilfe von sync.WaitGroup koordiniert. Dein Manager verfolgt, wann alle Downloads abgeschlossen sind, ohne auf willkürliche Sleep-Timer angewiesen zu sein.
Du organisierst deinen Code über zwei Dateien:
downloader.go: Definiere hier die Logik zur Koordination der Downloads.Erstelle eine
Download-Struktur mit den FeldernID(int),Filename(string) undSize(int, in KB).Implementiere eine Methode
Process(wg *sync.WaitGroup)fürDownload, die einen Download simuliert, indem sie für eine von der Dateigröße abhängige Dauer pausiert (verwendeSize * 10Millisekunden). Wenn der Download abgeschlossen ist, gib Folgendes aus:Downloaded: [Filename] ([Size]KB). Verwendedefer wg.Done(), um sicherzustellen, dass der Zähler der WaitGroup ordnungsgemäß verringert wird.Erstelle eine Funktion
StartDownloads(downloads []Download) int, die alle Downloads gleichzeitig mithilfe von Goroutinen startet und wartet, bis sie alle abgeschlossen sind. Denke daran, vor dem Start jeder Goroutinewg.Add(1)aufzurufen, nicht innerhalb der Goroutine. Gib die kombinierte Gesamtgröße aller Downloads zurück.main.go: Lies die Downloadinformationen ein und koordiniere die gleichzeitigen Downloads.Lies die Anzahl der Downloads ein und lies anschließend für jeden Download seine ID, seinen Dateinamen und seine Größe ein. Erstelle die Downloads und übergib sie an
StartDownloads. Gib nach Abschluss aller Downloads Folgendes aus:All downloads complete: [total]KB total
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Anzahl der Downloads (Ganzzahl)
- Folgende Zeilen: Für jeden Download drei Zeilen – die ID (Ganzzahl), der Dateiname (String) und die Größe in KB (Ganzzahl)
Zum Beispiel bei folgender Eingabe:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10Deine Ausgabe sollte zeigen, dass die Downloads abgeschlossen werden (kleinere Dateien werden aufgrund der kürzeren Pausenzeiten zuerst fertig), gefolgt von der Zusammenfassung:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB totalDer wesentliche Unterschied zur Verwendung von time.Sleep besteht darin, dass wg.Wait() blockiert, bis alle Goroutinen mit Done() ihre Fertigstellung signalisieren. Dadurch erhältst du eine präzise Synchronisierung, unabhängig davon, wie lange die einzelnen Downloads dauern.
Probier es selbst
package main
import (
"fmt"
)
func main() {
// Lese die Anzahl der Downloads
var n int
fmt.Scanln(&n)
// Lese Download-Informationen
downloads := make([]Download, n)
for i := 0; i < n; i++ {
var id int
var filename string
var size int
fmt.Scanln(&id)
fmt.Scanln(&filename)
fmt.Scanln(&size)
downloads[i] = Download{
ID: id,
Filename: filename,
Size: size,
}
}
// TODO: Rufe StartDownloads mit dem downloads-Slice auf
// TODO: Print the summary: "All downloads complete: [total]KB total"
}
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