Exemple pour sync.WaitGroup correct?

108

Cet exemple d'utilisation est-il sync.WaitGroupcorrect? Cela donne le résultat attendu, mais je ne suis pas sûr du wg.Add(4)et de la position de wg.Done(). Est-il judicieux d'ajouter les quatre goroutines à la fois avecwg.Add() ?

http://play.golang.org/p/ecvYHiie0P

package main

import (
    "fmt"
    "sync"
    "time"
)

func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    duration := millisecs * time.Millisecond
    time.Sleep(duration)
    fmt.Println("Function in background, duration:", duration)
    wg.Done()
}

func main() {
    var wg sync.WaitGroup
    wg.Add(4)
    go dosomething(200, &wg)
    go dosomething(400, &wg)
    go dosomething(150, &wg)
    go dosomething(600, &wg)

    wg.Wait()
    fmt.Println("Done")
}

Résultat (comme prévu):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done
topskip
la source
1
Que faire si dosomething () plante avant de pouvoir faire wg.Done ()?
Mostowski Collapse le
19
Je me rends compte que c'est vieux, mais pour les futurs gens, je recommanderais un premier defer wg.Done()appel au début de la fonction.
Brian

Réponses:

154

Oui, cet exemple est correct. Il est important que le wg.Add()se produise avant la godéclaration pour éviter les conditions de course. Ce qui suit serait également correct:

func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go dosomething(200, &wg)
    wg.Add(1)
    go dosomething(400, &wg)
    wg.Add(1)
    go dosomething(150, &wg)
    wg.Add(1)
    go dosomething(600, &wg)

    wg.Wait()
    fmt.Println("Done")
}

Cependant, il est plutôt inutile d'appeler wg.Addencore et encore lorsque vous savez déjà combien de fois il sera appelé.


Waitgroupspanique si le compteur tombe en dessous de zéro. Le compteur commence à zéro, chacun Done()est un -1et chacun Add()dépend du paramètre. Donc, pour s'assurer que le compteur ne tombe jamais en dessous et éviter les paniques, vous devez Add()être assuré de venir avant leDone() .

Dans Go, ces garanties sont données par le modèle de mémoire .

Le modèle de mémoire indique que toutes les instructions d'une même goroutine semblent être exécutées dans le même ordre qu'elles sont écrites. Il est possible qu'ils ne soient pas réellement dans cet ordre, mais le résultat sera comme si c'était le cas. Il est également garanti qu'un goroutine ne s'exécute qu'après l' goinstruction qui l'appelle . Puisque le Add()se produit avant l' goinstruction et que l' goinstruction se produit avant le Done(), nous connaissons leAdd() se produit avant le Done().

Si vous deviez faire passer l' goinstruction avant le Add(), le programme peut fonctionner correctement. Cependant, ce serait une condition de course car elle ne serait pas garantie.

Stephen Weinberg
la source
9
J'ai une question à propos de celle-ci: ne serait-il pas préférable de le faire defer wg.Done()pour être sûrs qu'il soit appelé quel que soit l'itinéraire emprunté par la goroutine? Merci.
Alessandro Santini
2
si vous vouliez simplement vous assurer que la fonction ne revenait pas avant la fin de toutes les routines go, alors yes defer serait préférable. en général, le but d'un groupe d'attente est d'attendre que tout le travail soit fait pour ensuite faire quelque chose avec les résultats que vous attendiez.
Zanven
1
Si vous ne l'utilisez pas deferet que l'un de vos goroutines ne parvient pas à appeler wg.Done()... ne bloquerez-vous pas pour Waittoujours? On dirait que cela pourrait facilement introduire un bogue difficile à trouver dans votre code ...
Dan Esparza
29

Je recommanderais d'intégrer l' wg.Add()appel dans ledoSomething() fonction elle-même, de sorte que si vous ajustez le nombre de fois où il est appelé, vous n'ayez pas à ajuster séparément le paramètre d'ajout manuellement, ce qui pourrait entraîner une erreur si vous en mettez à jour un mais oubliez de mettre à jour le other (dans cet exemple trivial qui est peu probable, mais je crois personnellement que c'est une meilleure pratique pour la réutilisation du code).

Comme le souligne Stephen Weinberg dans sa réponse à cette question , vous devez incrémenter le groupe d'attente avant de générer le gofunc, mais vous pouvez le faire facilement en enveloppant le spawn gofunc dans la doSomething()fonction elle-même, comme ceci:

func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
    wg.Add(1)
    go func() {
        duration := millisecs * time.Millisecond
        time.Sleep(duration)
        fmt.Println("Function in background, duration:", duration)
        wg.Done()
    }()
}

Ensuite, vous pouvez l'appeler sans l' goinvocation, par exemple:

func main() {
    var wg sync.WaitGroup
    dosomething(200, &wg)
    dosomething(400, &wg)
    dosomething(150, &wg)
    dosomething(600, &wg)
    wg.Wait()
    fmt.Println("Done")
}

Comme terrain de jeu: http://play.golang.org/p/WZcprjpHa_

mroth
la source
21
  • petite amélioration basée sur la réponse de Mroth
  • utiliser defer for Done est plus sûr
  func dosomething(millisecs time.Duration, wg *sync.WaitGroup) {
  wg.Add(1)
  go func() {
      defer wg.Done()
      duration := millisecs * time.Millisecond
      time.Sleep(duration)
      fmt.Println("Function in background, duration:", duration)
  }()
}

func main() {
  var wg sync.WaitGroup
  dosomething(200, &wg)
  dosomething(400, &wg)
  dosomething(150, &wg)
  dosomething(600, &wg)
  wg.Wait()
  fmt.Println("Done")
}
Bnaya
la source