sync.WaitGroup
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy — Lektion 64 von 107.
In der Lektion „Goroutines Basics“ haben wir time.Sleep verwendet, um auf das Beenden von Goroutinen zu warten – ein fragiler Ansatz. sync.WaitGroup bietet eine ordnungsgemäße Möglichkeit, auf den Abschluss der Arbeit einer Sammlung von Goroutinen zu warten.
Eine WaitGroup funktioniert wie ein Zähler. Sie erhöhen ihn, bevor Sie eine Goroutine starten, verringern ihn, wenn die Goroutine fertig ist, und blockieren, 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 der 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 dekrementiert wird, selbst wenn die Goroutine eine Panic auslöst.
Ein häufiger Fehler ist der Aufruf von Add innerhalb der Goroutine anstatt davor. Dies erzeugt eine Race Condition, bei der Wait möglicherweise zurückkehrt, bevor alle Goroutinen registriert sind:
// Falsch - Race Condition
go func() {
wg.Add(1) // könnte nach Wait() ausgeführt werden
defer wg.Done()
}()
// Richtig - Add vor dem Starten aufrufen
wg.Add(1)
go func() {
defer wg.Done()
}()WaitGroups werden oft in Structs eingebettet, um nebenläufige Operationen zu koordinieren, was sie für den Aufbau thread-sicherer Typen in Go unverzichtbar macht.
Aufgabe
EinfachLassen Sie uns einen Download-Manager erstellen, der mehrere gleichzeitige Dateidownloads mithilfe von sync.WaitGroup koordiniert. Ihr Manager wird verfolgen, wann alle Downloads abgeschlossen sind, ohne sich auf willkürliche Sleep-Timer zu verlassen.
Sie werden Ihren Code auf zwei Dateien aufteilen:
downloader.go: Definieren Sie Ihre Logik zur Download-Koordination.Erstellen Sie ein
Download-Struct mit den FeldernID(int),Filename(string) undSize(int, repräsentiert KB).Implementieren Sie eine Methode
Process(wg *sync.WaitGroup)fürDownload, die den Download simuliert, indem sie für eine Dauer basierend auf der Dateigröße schläft (verwenden SieSize * 10Millisekunden). Wenn der Download abgeschlossen ist, geben Sie aus:Downloaded: [Filename] ([Size]KB). Verwenden Siedefer wg.Done(), um sicherzustellen, dass der WaitGroup-Zähler ordnungsgemäß dekrementiert wird.Erstellen Sie eine Funktion
StartDownloads(downloads []Download) int, die alle Downloads gleichzeitig mithilfe von Goroutinen startet und wartet, bis alle abgeschlossen sind. Denken Sie daran,wg.Add(1)aufzurufen, bevor Sie jede Goroutine starten, nicht innerhalb dieser. Geben Sie die Gesamtgröße aller Downloads kombiniert zurück.main.go: Lesen Sie die Download-Informationen ein und orchestrieren Sie die gleichzeitigen Downloads.Lesen Sie die Anzahl der Downloads ein, und lesen Sie dann für jeden Download dessen ID, Dateinamen und Größe. Erstellen Sie die Downloads und übergeben Sie diese an
StartDownloads. Nachdem alle Downloads abgeschlossen sind, geben Sie 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, gegeben:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10Ihre Ausgabe sollte den Abschluss der Downloads zeigen (kleinere Dateien werden aufgrund kürzerer Sleep-Zeiten zuerst fertig), gefolgt von der Zusammenfassung:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB totalDer entscheidende Unterschied zur Verwendung von time.Sleep besteht darin, dass wg.Wait() blockiert, bis alle Goroutinen den Abschluss mit Done() signalisieren, was Ihnen eine präzise Synchronisation ermöglicht, unabhängig davon, wie lange jeder Download dauert.
Spickzettel
Ein sync.WaitGroup bietet eine ordnungsgemäße Möglichkeit, auf den Abschluss von Goroutinen zu warten. Es funktioniert wie ein Zähler, der laufende Goroutinen verfolgt.
Die drei wichtigsten Methoden sind:
Add(n)- erhöht den Zähler um nDone()- verringert den Zähler um eins (entsprichtAdd(-1))Wait()- blockiert, bis der Zähler Null erreicht
Grundlegendes Nutzungsmuster:
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // Inkrementieren vor dem Starten der Goroutine
go func(id int) {
defer wg.Done() // Dekrementieren, wenn fertig
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // blockieren, bis alle Goroutinen abgeschlossen sind
fmt.Println("All workers complete")Die Verwendung von defer wg.Done() stellt sicher, dass der Zähler auch dann dekrementiert wird, wenn die Goroutine eine Panic auslöst.
Wichtig: Rufen Sie Add immer vor dem Starten der Goroutine auf, nicht innerhalb dieser, um Race Conditions zu vermeiden:
// Falsch - Race Condition
go func() {
wg.Add(1) // könnte nach Wait() ausgeführt werden
defer wg.Done()
}()
// Richtig
wg.Add(1)
go func() {
defer wg.Done()
}()Probier es selbst
package main
import (
"fmt"
)
func main() {
// Anzahl der Downloads lesen
var n int
fmt.Scanln(&n)
// Download-Informationen lesen
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: StartDownloads mit dem downloads-Slice aufrufen
// TODO: Zusammenfassung ausgeben: "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 Go OOP
Externe DateienGo Workspace & ModulePackages & ImportsExportierte vs. nicht-exportierte NamenEinführung in OOP in GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktor-FunktionenZusammenfassung – Einfacher Taschenrechner4Interfaces
Einführung in InterfacesImplizite ImplementierungInterface als VertragLeeres Interface (any)Type AssertionType SwitchInterface-KompositionStringer & Error InterfacesRückblick - Formen-Rechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf Paket-EbeneGetter- & Setter-MethodenInformation Hiding in GoZusammenfassung - Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterType ConstraintsGenerische StructsWorkaround für generische MethodenZusammenfassung – Generische Collection2Vertiefung: Typen & Structs
Basis- & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct TagsAnonyme StructsVerschachtelte StructsZero Values & StandardwerteZusammenfassung - Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen des Struct EmbeddingMethod PromotionEinbetten mehrerer StructsEmbedding vs. AggregationShadowing eingebetteter MethodenZusammenfassung – Mitarbeiter-Hierarchie8Fehlerbehandlung & OOP
Das error-InterfaceEigene FehlertypenError Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverZusammenfassung – Datei-Parser3Zeiger & Speicher
Zeiger-Grundlagen in GoZeiger auf StructsWertübergabe vs. ReferenzübergabeDie new() FunktionGarbage Collection in GoWiederholung – Linked List Builder6Polymorphismus in Go
Polymorphismus über InterfacesDuck Typing in GoInterface-ErfüllungsregelnPolymorphe CollectionsDependency InjectionZusammenfassung – Payment Processor9Nebenläufigkeit & OOP
Goroutines GrundlagenChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignZusammenfassung - Worker Pool