Menu

WaitGroup w Golang: Add, Done, Wait i pula workerów

Jak sync.WaitGroup czeka na zakończenie grupy goroutine: zasady Add, Done i Wait, dlaczego trzeba go przekazywać przez wskaźnik, zbieranie wyników i błędów oraz zbudowana na nim pula workerów.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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:

  • main musi odbierać wyniki, gdy workerzy działają. Gdyby main wywołał wg.Wait() bezpośrednio przed czytaniem, workerzy zablokowaliby się na wysyłaniu do results, nigdy nie doszliby do Done i wszystko skończyłoby się zakleszczeniem. Dlatego Wait działa we własnej goroutine.
  • close(results) następuje dopiero po Wait, 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

PotrzebaUżyj
Czekanie na N goroutine, wyniki w indeksowanych miejscachsync.WaitGroup
Czekanie na jedną goroutinekanał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łędzieerrgroup.WithContext
Zatrzymanie wszystkiego po timeoucie lub anulowaniu przez wywołującegocontext.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. Wait może się zakończyć, zanim to się wykona.
  • Brak Done przy wcześniejszym powrocie. Zawsze defer wg.Done().
  • Przekazywanie WaitGroup przez wartość. Użyj wskaźnika; go vet zgłasza kopię.
  • Czekanie w tej samej goroutine, która musi opróżniać kanał. Przenieś wg.Wait() razem z close do osobnej goroutine.
  • Ponowne użycie WaitGroup, zanim poprzednie Wait się zakończyło. Nowy cykl wywołań Add zaczynaj dopiero po zakończeniu Wait.

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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ