Podstawowy wzorzec
sync.WaitGroup liczy działające goroutine. Add zwiększa licznik, Done go zmniejsza, a Wait blokuje, dopóki nie wyniesie zera.
Trzy pobierania działają współbieżnie, więc program trwa około 10 ms zamiast 30. Wyniki są wypisywane w kolejności wejścia, bo każda goroutine zapisuje tylko własny indeks w sizes, a main odczytuje je dopiero po Wait.
Wartość zerowa WaitGroup jest gotowa do użycia. Bez konstruktora.
Trzy zasady
Wywołuj Add przed go, a nie wewnątrz goroutine. Jeśli goroutine sama wywołuje Add, main może dojść do Wait, zanim którakolwiek goroutine wystartuje, zobaczyć licznik równy zero i zakończyć działanie, zanim praca się zaczęła. Gdy znasz liczbę z góry, jedno wg.Add(len(files)) przed pętlą działa tak samo.
Wywołuj Done przez defer w pierwszej linii goroutine. Goroutine, która wraca wcześniej z powodu błędu albo wywołuje panic, i tak zmniejsza licznik. Brakujące Done zostawia Wait zablokowane na zawsze. Jeśli to jedyna pozostała goroutine, runtime zgłasza fatal error: all goroutines are asleep z sync.WaitGroup.Wait w śladzie stosu.
Nigdy nie kopiuj WaitGroup po pierwszym użyciu. Przekazuj do funkcji *sync.WaitGroup albo przechwyć zmienną w domknięciu, jak powyżej.
Przekazywanie WaitGroup do funkcji
Gdy ciało goroutine to nazwana funkcja, przekaż wskaźnik:
Z parametrem wg sync.WaitGroup przekazywanym przez wartość każdy worker wywoływałby Done na własnej kopii, a main blokowałby się w Wait na zawsze. go vet wyłapuje to, zanim cokolwiek uruchomisz:
./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
Czystszy projekt w ogóle trzyma współbieżność poza worker: niech to będzie zwykła funkcja, a księgowość Add/Done odbywa się w domknięciu po stronie wywołującego. Wtedy worker łatwo przetestować i wywołać synchronicznie.
Ujemny licznik
Done to Add(-1). Jeśli licznik spadnie poniżej zera, program wywołuje panic:
Wynik to recovered: sync: negative WaitGroup counter. Zwykle przyczyną jest goroutine z defer wg.Done(), która na jakiejś ścieżce wywołuje też jawnie wg.Done().
Zbieranie błędów
WaitGroup tylko liczy. Dla błędów daj każdej goroutine własne miejsce i sprawdź je po Wait:
errors.Join (Go 1.20) pomija wartości nil i zwraca nil, jeśli wszystkie są nil, więc łączy zasadę „jeden błąd na goroutine” bez dodatkowej księgowości.
Jeśli chcesz zatrzymać pozostałą pracę, gdy tylko jedna goroutine zawiedzie, użyj zamiast tego golang.org/x/sync/errgroup. To WaitGroup plus pierwszy błąd plus kontekst anulowany przy porażce, a g.SetLimit(n) ogranicza współbieżność. Pakiet jest poza biblioteką standardową, więc nie da się go uruchomić w edytorze na tej stronie:
g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
return err // the first error; ctx was cancelled for the others
}
Pula workerów
Stała liczba goroutine czytających zadania z kanału utrzymuje ograniczoną współbieżność bez względu na liczbę zadań. WaitGroup mówi, kiedy wszyscy workerzy skończyli, czyli kiedy można zamknąć kanał wyników.
Kolejność tych trzech elementów ma znaczenie:
mainmusi odbierać wyniki, gdy workerzy działają. Gdybymainwywołałwg.Wait()bezpośrednio przed czytaniem, workerzy zablokowaliby się na wysyłaniu doresults, nigdy nie doszliby doDonei wszystko skończyłoby się zakleszczeniem. DlategoWaitdziała we własnej goroutine.close(results)następuje dopiero poWait, więc żaden worker nie może wysłać do zamkniętego kanału.- Podawanie zadań też odbywa się w goroutine, więc podawanie i zbieranie się nakładają.
To, który worker obsłużył które zadanie, zmienia się przy każdym uruchomieniu, więc program przed wypisaniem sortuje po zadaniu. Wszystko, co wypisuje, jest deterministyczne.
WaitGroup, kanał czy errgroup
| Potrzeba | Użyj |
|---|---|
| Czekanie na N goroutine, wyniki w indeksowanych miejscach | sync.WaitGroup |
| Czekanie na jedną goroutine | kanału done albo samego kanału wyników |
| Wyniki przesyłane strumieniowo, gdy się kończą | kanału zamykanego po wg.Wait() |
| Zatrzymanie wszystkiego przy pierwszym błędzie | errgroup.WithContext |
| Zatrzymanie wszystkiego po timeoucie lub anulowaniu przez wywołującego | context.Context plus WaitGroup lub errgroup |
Go 1.25 dodaje wg.Go(func() { ... }), które robi za ciebie Add(1) i odroczone Done. Kod dla Go 1.24 i wcześniejszych, łącznie z edytorem na tej stronie, używa jawnej formy pokazanej powyżej.
Typowe błędy
wg.Add(1)wewnątrz goroutine.Waitmoże się zakończyć, zanim to się wykona.- Brak
Doneprzy wcześniejszym powrocie. Zawszedefer wg.Done(). - Przekazywanie WaitGroup przez wartość. Użyj wskaźnika;
go vetzgłasza kopię. - Czekanie w tej samej goroutine, która musi opróżniać kanał. Przenieś
wg.Wait()razem zclosedo osobnej goroutine. - Ponowne użycie WaitGroup, zanim poprzednie
Waitsię zakończyło. Nowy cykl wywołańAddzaczynaj dopiero po zakończeniuWait.
Najczęściej zadawane pytania
Jak działa sync.WaitGroup w Go?
WaitGroup to licznik. wg.Add(n) go zwiększa, wg.Done() zmniejsza o jeden, a wg.Wait() blokuje, dopóki nie osiągnie zera. Wywołaj Add przed uruchomieniem każdej goroutine, defer wg.Done() wewnątrz niej, a Wait tam, gdzie wszystko musi być skończone.
Czy WaitGroup przekazywać przez wartość, czy przez wskaźnik?
Przez wskaźnik (*sync.WaitGroup) albo pozwól goroutine przechwycić go w domknięciu. Kopia ma własny licznik, więc Done na kopii nigdy nie dociera do oryginału, a Wait blokuje na zawsze. go vet zgłasza ten błąd jako "passes lock by value".
Co powoduje "sync: negative WaitGroup counter"?
Więcej wywołań Done niż Add. Zwykle goroutine wywołuje Done dwa razy (raz przez defer i raz jawnie) albo na jednej ze ścieżek pominięto Add(1). Program wywołuje panic, bo licznik nie może już powiedzieć niczego prawdziwego.
Jak odebrać błędy z goroutine uruchomionych z WaitGroup?
WaitGroup nie przenosi wyników ani błędów. Daj każdej goroutine własne miejsce w slice'ie błędów i połącz je po Wait (na przykład przez errors.Join) albo użyj golang.org/x/sync/errgroup, którego Wait zwraca pierwszy błąd i może anulować pozostałe przez kontekst.