sync.WaitGroup
Część sekcji Programowanie obiektowe ścieżki GO w Coddy. Lekcja 64 z 107.
W lekcji Podstawy goroutines użyliśmy time.Sleep, aby poczekać na zakończenie goroutines — to zawodne podejście. sync.WaitGroup zapewnia właściwy sposób oczekiwania na wykonanie pracy przez grupę goroutines.
WaitGroup działa jak licznik. Zwiększasz jego wartość przed uruchomieniem gorutyny, zmniejszasz ją, gdy gorutyna zakończy działanie, i blokujesz wykonanie, aż licznik osiągnie zero:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // zwiększ licznik
go func(id int) {
defer wg.Done() // zmniejsz licznik po zakończeniu
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // czekaj, aż licznik osiągnie 0
fmt.Println("All workers complete")
}Trzy kluczowe metody to Add(n), która zwiększa licznik o n, Done(), która zmniejsza go o jeden (równoważna Add(-1)), oraz Wait(), która blokuje działanie do chwili, gdy licznik osiągnie zero. Użycie defer wg.Done() zapewnia zmniejszenie licznika nawet wtedy, gdy gorutyna wywoła panikę.
Częstym błędem jest wywoływanie Add wewnątrz gorutyny zamiast przed jej uruchomieniem. Powoduje to wyścig, w którym Wait może zakończyć działanie, zanim wszystkie gorutyny zostaną zarejestrowane:
// Niepoprawnie – wyścig danych
go func() {
wg.Add(1) // może wykonać się po Wait()
defer wg.Done()
}()
// Poprawnie – Add przed uruchomieniem
wg.Add(1)
go func() {
defer wg.Done()
}()WaitGroupy są często osadzane w strukturach, aby koordynować współbieżne operacje, dzięki czemu są niezbędne do tworzenia bezpiecznych wątkowo typów w Go.
Wyzwanie
ŁatwyZbudujmy menedżera pobierania, który koordynuje wiele równoczesnych pobrań plików za pomocą sync.WaitGroup. Menedżer będzie śledzić, kiedy wszystkie pobrania się zakończą, bez polegania na arbitralnych timerach uśpienia.
Podzielisz kod na dwa pliki:
downloader.go: Zdefiniuj logikę koordynującą pobieranie.Utwórz strukturę
Downloadz polamiID(int),Filename(string) iSize(int, oznaczającym rozmiar w KB).Zaimplementuj metodę
Process(wg *sync.WaitGroup)dlaDownload, która symuluje pobieranie, usypiając na czas zależny od rozmiaru pliku (użyjSize * 10milisekund). Po zakończeniu pobierania wypisz:Downloaded: [Filename] ([Size]KB). Użyjdefer wg.Done(), aby licznik WaitGroup został prawidłowo zmniejszony.Utwórz funkcję
StartDownloads(downloads []Download) int, która uruchamia wszystkie pobrania równocześnie za pomocą gorutyn i czeka na ich zakończenie. Pamiętaj, aby wywołaćwg.Add(1)przed uruchomieniem każdej gorutyny, a nie wewnątrz niej. Zwróć łączny rozmiar wszystkich pobrań.main.go: Wczytaj informacje o pobieranych plikach i zorganizuj równoczesne pobieranie.Wczytaj liczbę pobrań, a następnie dla każdego pobrania wczytaj jego ID, nazwę pliku i rozmiar. Utwórz pobrania i przekaż je do
StartDownloads. Po zakończeniu wszystkich pobrań wypisz:All downloads complete: [total]KB total
Dane wejściowe będą miały następującą postać:
- Wiersz 1: Liczba pobrań (liczba całkowita)
- Kolejne wiersze: Dla każdego pobrania trzy wiersze — ID (liczba całkowita), nazwa pliku (ciąg znaków) i rozmiar w KB (liczba całkowita)
Na przykład dla danych:
3
1
report.pdf
20
2
image.png
5
3
data.csv
10Wynik powinien pokazywać kończące się pobrania (mniejsze pliki kończą się jako pierwsze ze względu na krótszy czas uśpienia), a następnie podsumowanie:
Downloaded: image.png (5KB)
Downloaded: data.csv (10KB)
Downloaded: report.pdf (20KB)
All downloads complete: 35KB totalGłówna różnica w porównaniu z użyciem time.Sleep polega na tym, że wg.Wait() blokuje działanie do chwili, gdy wszystkie gorutyny zasygnalizują zakończenie za pomocą Done(), zapewniając precyzyjną synchronizację niezależnie od czasu trwania każdego pobrania.
Spróbuj swoich sił
package main
import (
"fmt"
)
func main() {
// Odczytaj liczbę pobrań
var n int
fmt.Scanln(&n)
// Odczytaj informacje o pobraniach
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: Wywołaj StartDownloads z wycinkiem downloads
// TODO: Wyświetl podsumowanie: "All downloads complete: [total]KB total"
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy OOP w Go
Pliki zewnętrzneObszar roboczy i moduły GoPakiety i importyNazwy eksportowane i nieeksportowaneWprowadzenie do OOP w GoStruktury jako klasyDefiniowanie metod dla strukturOdbiorniki wskaźnikowe i wartościoweInicjalizacja strukturFunkcje konstruktoraPodsumowanie – prosty kalkulator4Interfejsy
Wprowadzenie do interfejsówImplementacja niejawnaInterfejs jako kontraktPusty interfejs (any)Asercja typuPrzełącznik typówKompozycja interfejsówInterfejsy Stringer i ErrorPowtórka — kalkulator figur7Hermetyzacja
Pola eksportowane i nieeksportowaneHermetyzacja na poziomie pakietuMetody pobierające i ustawiająceUkrywanie informacji w GoPowtórka – dane studentów10Typy generyczne (Go 1.18+)
Wprowadzenie do typów generycznychParametry typówOgraniczenia typówStruktury generyczneObejście ograniczeń metod generycznychPodsumowanie — generyczna kolekcja2Typy i struktury w praktyce
Typy proste i złożoneDefiniowanie własnych typówTagi pól strukturStruktury anonimoweStruktury zagnieżdżoneWartości zerowe i domyślnePodsumowanie – książka kontaktów5Kompozycja zamiast dziedziczenia
Dlaczego Go nie ma dziedziczeniaPodstawy osadzania strukturPromowanie metodOsadzanie wielu strukturOsadzanie a agregacjaPrzesłanianie osadzonych metodPodsumowanie — hierarchia pracowników8Obsługa błędów i programowanie obiektowe
Interfejs błęduWłasne typy błędówOpakowywanie błędów (fmt.Errorf)Błędy wartowniczeerrors.Is() i errors.As()Panic, Defer i RecoverPodsumowanie — parser plików3Wskaźniki i pamięć
Podstawy wskaźników w GoWskaźniki do strukturPrzekazywanie przez wartość a przez referencjęFunkcja new()Odśmiecanie pamięci w GoPowtórka — kreator listy wiązanej6Polimorfizm w Go
Polimorfizm za pomocą interfejsówDuck typing w GoZasady spełniania interfejsówKolekcje polimorficzneWstrzykiwanie zależnościPowtórka — procesor płatności9Współbieżność i OOP
Podstawy goroutinesKanały i komunikacjaKanały buforowane i niebuforowaneInstrukcja selectsync.Mutex i sync.RWMutexsync.WaitGroupProjektowanie struktur bezpiecznych wątkowoPowtórka – pula workerów12Zaawansowane zagadnienia programowania obiektowego
Wzorzec opcji funkcyjnychWzorzec Builder w GoŁańcuchowe wywoływanie metodAliasy typów a definicje typówPodstawy refleksjiPrzegląd generowania koduPoćwicz samodzielnie: Kompilator Go online