Menu

WaitGroup en Golang : Add, Done, Wait et pool de workers

Comment sync.WaitGroup attend la fin d'un ensemble de goroutines : les règles d'Add, Done et Wait, pourquoi il doit être passé par pointeur, la collecte des résultats et des erreurs, et un pool de workers construit dessus.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Le motif de base

sync.WaitGroup compte les goroutines en cours. Add augmente le compte, Done le diminue, Wait bloque jusqu'à ce qu'il vaille zéro.

Les trois téléchargements s'exécutent en concurrence, donc le programme prend environ 10 ms au lieu de 30. Les résultats s'affichent dans l'ordre d'entrée parce que chaque goroutine n'écrit que son propre indice de sizes, et que main ne les lit qu'après Wait.

La valeur zéro d'un WaitGroup est prête à l'emploi. Pas de constructeur.

Les trois règles

Appelez Add avant go, pas dans la goroutine. Si la goroutine appelle Add elle-même, main peut atteindre Wait avant qu'aucune goroutine n'ait démarré, voir un compte à zéro, et retourner alors que le travail n'a pas commencé. Quand vous connaissez le compte à l'avance, un seul wg.Add(len(files)) avant la boucle est équivalent.

Appelez Done avec defer en première ligne de la goroutine. Une goroutine qui retourne tôt sur une erreur, ou qui provoque un panic, décrémente quand même le compteur. Un Done manquant laisse Wait bloqué indéfiniment. Si c'est la seule goroutine restante, le runtime signale fatal error: all goroutines are asleep avec sync.WaitGroup.Wait dans la trace.

Ne copiez jamais un WaitGroup après sa première utilisation. Passez *sync.WaitGroup aux fonctions, ou capturez la variable dans une closure comme ci-dessus.

Passer un WaitGroup à une fonction

Quand le corps de la goroutine est une fonction nommée, passez un pointeur :

Avec wg sync.WaitGroup comme paramètre valeur, chaque worker appellerait Done sur sa propre copie et main bloquerait indéfiniment dans Wait. go vet le détecte avant toute exécution :

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

Une conception plus propre garde la concurrence entièrement hors de worker : laissez-la être une fonction ordinaire et faites la comptabilité Add/Done dans la closure de l'appelant. worker est alors facile à tester et à appeler de façon synchrone.

Compteur négatif

Done équivaut à Add(-1). Si le compte passe sous zéro, le programme provoque un panic :

La sortie est recovered: sync: negative WaitGroup counter. La cause habituelle est une goroutine avec defer wg.Done() qui appelle aussi wg.Done() explicitement sur un chemin.

Collecter les erreurs

Un WaitGroup ne fait que compter. Pour les erreurs, donnez à chaque goroutine sa propre case et examinez-les après Wait :

errors.Join (Go 1.20) ignore les valeurs nil et renvoie nil si toutes valent nil, donc il combine « une erreur par goroutine » sans comptabilité supplémentaire.

Si vous voulez arrêter le travail restant dès qu'une goroutine échoue, utilisez plutôt golang.org/x/sync/errgroup. C'est un WaitGroup plus la première erreur plus un context annulé en cas d'échec, et g.SetLimit(n) plafonne la concurrence. Il vit hors de la bibliothèque standard, donc il ne peut pas s'exécuter dans l'éditeur de cette page :

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 pool de workers

Un nombre fixe de goroutines qui lisent des tâches depuis un channel garde la concurrence bornée, quel que soit le nombre de tâches. Le WaitGroup vous dit quand tous les workers ont fini, c'est-à-dire quand le channel de résultats peut être fermé.

L'ordre des trois éléments compte :

  • main doit recevoir les résultats pendant que les workers tournent. Si main appelait wg.Wait() directement avant de lire, les workers bloqueraient en envoyant sur results, n'atteindraient jamais Done, et tout finirait en deadlock. C'est pourquoi Wait s'exécute dans sa propre goroutine.
  • close(results) n'a lieu qu'après Wait, donc aucun worker ne peut envoyer sur un channel fermé.
  • L'alimentation des tâches s'exécute aussi dans une goroutine, donc l'alimentation et la collecte se chevauchent.

Quel worker a traité quelle tâche change d'une exécution à l'autre, donc le programme trie par tâche avant d'afficher. Tout ce qu'il affiche est déterministe.

WaitGroup, channel ou errgroup

BesoinÀ utiliser
Attendre N goroutines, résultats dans des cases indexéessync.WaitGroup
Attendre une seule goroutineun channel done ou le channel de résultat lui-même
Résultats transmis au fur et à mesureun channel, fermé après wg.Wait()
Tout arrêter à la première erreurerrgroup.WithContext
Tout arrêter sur un timeout ou une annulation de l'appelantcontext.Context plus un WaitGroup ou un errgroup

Go 1.25 ajoute wg.Go(func() { ... }), qui fait le Add(1) et le Done différé à votre place. Le code pour Go 1.24 et avant, y compris l'éditeur de cette page, utilise la forme explicite montrée plus haut.

Erreurs courantes

  • wg.Add(1) dans la goroutine. Wait peut retourner avant qu'il ne s'exécute.
  • Oublier Done lors d'un retour anticipé. Faites toujours defer wg.Done().
  • Passer le WaitGroup par valeur. Utilisez un pointeur ; go vet signale la copie.
  • Attendre dans la goroutine même qui doit vider un channel. Déplacez wg.Wait() et le close dans une goroutine séparée.
  • Réutiliser un WaitGroup avant le retour du Wait précédent. Ne commencez un nouveau cycle d'appels à Add qu'une fois Wait terminé.

Questions fréquentes

Comment fonctionne sync.WaitGroup en Go ?

Un WaitGroup est un compteur. wg.Add(n) l'augmente, wg.Done() le diminue de un, et wg.Wait() bloque jusqu'à ce qu'il atteigne zéro. Appelez Add avant de lancer chaque goroutine, defer wg.Done() à l'intérieur, et Wait là où vous avez besoin que tout soit terminé.

Faut-il passer un WaitGroup par valeur ou par pointeur ?

Par pointeur (*sync.WaitGroup), ou laissez les goroutines le capturer dans une closure. Une copie a son propre compteur, donc Done sur la copie n'atteint jamais l'original et Wait bloque indéfiniment. go vet signale l'erreur par « passes lock by value ».

Qu'est-ce qui cause « sync: negative WaitGroup counter » ?

Plus d'appels à Done que d'appels à Add. En général, une goroutine appelle Done deux fois (une fois avec defer et une fois explicitement), ou Add(1) est sauté sur un chemin. Le programme provoque un panic, puisque le compteur ne peut plus rien dire de vrai.

Comment récupérer les erreurs des goroutines lancées avec un WaitGroup ?

Un WaitGroup ne transporte ni résultats ni erreurs. Donnez à chaque goroutine sa propre case dans une slice d'erreurs et combinez-les après Wait (par exemple avec errors.Join), ou utilisez golang.org/x/sync/errgroup, dont le Wait renvoie la première erreur et peut annuler les autres via un context.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER