Menu

WaitGroup in Golang: Add, Done, Wait e un worker pool

Come sync.WaitGroup aspetta che un gruppo di goroutine finisca: le regole di Add, Done e Wait, perché va passato per puntatore, come raccogliere risultati ed errori e un worker pool costruito su di esso.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Lo schema di base

sync.WaitGroup conta le goroutine in esecuzione. Add aumenta il conteggio, Done lo diminuisce, Wait si blocca finché non arriva a zero.

I tre download girano in parallelo, quindi il programma impiega circa 10 ms invece di 30. I risultati vengono stampati nell'ordine dell'input perché ogni goroutine scrive solo il proprio indice di sizes, e main li legge solo dopo Wait.

Il valore zero di un WaitGroup è pronto all'uso. Nessun costruttore.

Le tre regole

Chiama Add prima di go, non dentro la goroutine. Se è la goroutine a chiamare Add, main può arrivare a Wait prima che qualsiasi goroutine sia partita, vedere un conteggio pari a zero e ritornare quando il lavoro non è nemmeno iniziato. Quando conosci il numero in anticipo, un solo wg.Add(len(files)) prima del ciclo è equivalente.

Chiama Done con defer come prima riga della goroutine. Una goroutine che ritorna in anticipo per un errore, o che va in panic, decrementa comunque il contatore. Un Done mancante lascia Wait bloccato per sempre. Se è l'unica goroutine rimasta, il runtime segnala fatal error: all goroutines are asleep con sync.WaitGroup.Wait nello stack trace.

Non copiare mai un WaitGroup dopo il primo utilizzo. Passa *sync.WaitGroup alle funzioni, oppure cattura la variabile in una closure come sopra.

Passare un WaitGroup a una funzione

Quando il corpo della goroutine è una funzione con nome, passa un puntatore:

Con wg sync.WaitGroup come parametro per valore, ogni worker chiamerebbe Done sulla propria copia e main resterebbe bloccato in Wait per sempre. go vet lo rileva prima che tu esegua qualsiasi cosa:

./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy

Un design più pulito tiene la concorrenza del tutto fuori da worker: lasciala una funzione normale e gestisci Add/Done nella closure del chiamante. Così worker è facile da testare e da chiamare in modo sincrono.

Contatore negativo

Done equivale ad Add(-1). Se il conteggio scende sotto zero, il programma va in panic:

L'output è recovered: sync: negative WaitGroup counter. La causa tipica è una goroutine con defer wg.Done() che in qualche ramo chiama anche wg.Done() esplicitamente.

Raccogliere gli errori

Un WaitGroup si limita a contare. Per gli errori, dai a ogni goroutine la sua posizione e controllale dopo Wait:

errors.Join (Go 1.20) salta i valori nil e restituisce nil se sono tutti nil, quindi combina "un errore per goroutine" senza alcuna gestione aggiuntiva.

Se vuoi fermare il lavoro rimanente appena una goroutine fallisce, usa invece golang.org/x/sync/errgroup. È un WaitGroup più il primo errore più un context che viene cancellato in caso di fallimento, e g.SetLimit(n) limita la concorrenza. Sta fuori dalla libreria standard, quindi non può girare nell'editor di questa pagina:

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
}

Un worker pool

Un numero fisso di goroutine che leggono job da un canale mantiene limitata la concorrenza, qualunque sia il numero di job. Il WaitGroup ti dice quando tutti i worker hanno finito, cioè quando si può chiudere il canale dei risultati.

L'ordine dei tre pezzi è importante:

  • main deve ricevere i risultati mentre i worker girano. Se main chiamasse wg.Wait() direttamente prima di leggere, i worker resterebbero bloccati nell'invio su results, non arriverebbero mai a Done e tutto andrebbe in deadlock. Ecco perché Wait gira in una goroutine a parte.
  • close(results) avviene solo dopo Wait, quindi nessun worker può inviare su un canale chiuso.
  • Anche chi fornisce i job gira in una goroutine, così l'invio e la raccolta si sovrappongono.

Quale worker gestisce quale job cambia da un'esecuzione all'altra, quindi il programma ordina per job prima di stampare. Tutto ciò che stampa è deterministico.

WaitGroup, canale o errgroup

EsigenzaUsa
Aspettare N goroutine, risultati in posizioni indicizzatesync.WaitGroup
Aspettare una sola goroutineun canale done o il canale dei risultati stesso
Risultati trasmessi man mano che finisconoun canale, chiuso dopo wg.Wait()
Fermare tutto al primo erroreerrgroup.WithContext
Fermare tutto per un timeout o una cancellazione del chiamantecontext.Context più un WaitGroup o un errgroup

Go 1.25 aggiunge wg.Go(func() { ... }), che esegue per te Add(1) e il Done differito. Il codice per Go 1.24 e versioni precedenti, compreso l'editor di questa pagina, usa la forma esplicita mostrata sopra.

Errori comuni

  • wg.Add(1) dentro la goroutine. Wait può ritornare prima che venga eseguito.
  • Dimenticare Done in un ritorno anticipato. Usa sempre defer wg.Done().
  • Passare il WaitGroup per valore. Usa un puntatore; go vet segnala la copia.
  • Aspettare nella stessa goroutine che deve svuotare un canale. Sposta wg.Wait() e il close in una goroutine separata.
  • Riusare un WaitGroup prima che il Wait precedente sia ritornato. Inizia un nuovo ciclo di chiamate ad Add solo dopo che Wait ha finito.

Domande frequenti

Come funziona sync.WaitGroup in Go?

Un WaitGroup è un contatore. wg.Add(n) lo incrementa, wg.Done() lo decrementa di uno e wg.Wait() si blocca finché non arriva a zero. Chiama Add prima di avviare ogni goroutine, defer wg.Done() al suo interno e Wait nel punto in cui ti serve che tutto sia finito.

Un WaitGroup va passato per valore o per puntatore?

Per puntatore (*sync.WaitGroup), oppure lascia che le goroutine lo catturino in una closure. Una copia ha il proprio contatore, quindi Done sulla copia non raggiunge mai l'originale e Wait resta bloccato per sempre. go vet segnala l'errore come "passes lock by value".

Cosa causa "sync: negative WaitGroup counter"?

Più chiamate a Done che ad Add. Di solito una goroutine chiama Done due volte (una con defer e una esplicitamente), oppure Add(1) viene saltato in un ramo. Il programma va in panic, perché il contatore non può più dirti nulla di vero.

Come ottengo gli errori dalle goroutine avviate con un WaitGroup?

Un WaitGroup non trasporta né risultati né errori. Dai a ogni goroutine la sua posizione in uno slice di errori e combinali dopo Wait (per esempio con errors.Join), oppure usa golang.org/x/sync/errgroup, il cui Wait restituisce il primo errore e può cancellare le altre goroutine tramite un context.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA