Beispiel für sync.WaitGroup richtig?

108

Ist diese Beispielverwendung sync.WaitGroupkorrekt? Es gibt das erwartete Ergebnis, aber ich bin mir nicht sicher über die wg.Add(4)und die Position von wg.Done(). Ist es sinnvoll, die vier Goroutinen gleichzeitig mit hinzuzufügen wg.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")
}

Ergebnis (wie erwartet):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done
topskip
quelle
1
Was ist, wenn dosomething () abstürzt, bevor es wg.Done () ausführen kann?
Mostowski Zusammenbruch
19
Mir ist klar, dass dies alt ist, aber für zukünftige Leute würde ich einen ersten defer wg.Done()Aufruf zu Beginn der Funktion empfehlen .
Brian

Antworten:

154

Ja, dieses Beispiel ist korrekt. Es ist wichtig, dass das wg.Add()vor der goAussage passiert , um Rennbedingungen zu verhindern. Folgendes wäre auch richtig:

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")
}

Es ist jedoch ziemlich sinnlos, immer wieder anzurufen, wg.Addwenn Sie bereits wissen, wie oft es angerufen wird.


WaitgroupsPanik, wenn der Zähler unter Null fällt. Der Zähler beginnt bei Null, jeder Done()ist a -1und jeder Add()hängt vom Parameter ab. Also, um sicherzustellen , dass der Zähler nie unter Tropfen und vermeiden Sie gerät in Panik, müssen Sie die Add()zu gewährleistet , bevor die kommenDone() .

In Go werden solche Garantien vom Speichermodell gegeben .

Das Speichermodell besagt, dass alle Anweisungen in einer einzelnen Goroutine in derselben Reihenfolge ausgeführt werden, in der sie geschrieben wurden. Es ist möglich, dass sie nicht in dieser Reihenfolge sind, aber das Ergebnis wird so sein, als ob es so wäre. Es ist auch garantiert, dass eine Goroutine erst nach der goAnweisung ausgeführt wird, die sie aufruft . Da das Add()vor der goAnweisung und die goAnweisung vor dem auftritt Done(), wissen wir, dass das Add()vor dem auftritt Done().

Wenn Sie die goAnweisung vor dem haben Add(), funktioniert das Programm möglicherweise ordnungsgemäß. Es wäre jedoch eine Rennbedingung, da dies nicht garantiert werden würde.

Stephen Weinberg
quelle
9
Ich habe eine Frage zu dieser Frage: defer wg.Done()Wäre es nicht besser, wenn wir sicher sind, dass sie unabhängig von der Route der Goroutine aufgerufen wird? Vielen Dank.
Alessandro Santini
2
Wenn Sie nur sicherstellen möchten, dass die Funktion nicht zurückgegeben wird, bevor alle Go-Routinen abgeschlossen sind, wird Ja-Zurückstellen bevorzugt. Normalerweise besteht der gesamte Sinn einer Wartegruppe darin, zu warten, bis die gesamte Arbeit erledigt ist, um dann etwas mit den Ergebnissen zu tun, auf die Sie gewartet haben.
Zanven
1
Wenn Sie nicht verwenden deferund eine Ihrer Goroutinen nicht anruft wg.Done()... Waitblockieren Sie nicht einfach für immer? Das klingt so, als könnte es leicht zu einem schwer zu findenden Fehler in Ihrem Code kommen ...
Dan Esparza
29

Ich würde empfehlen, den wg.Add()Aufruf in die doSomething()Funktion selbst einzubetten , damit Sie den Parameter add nicht manuell anpassen müssen, wenn Sie die Anzahl der Aufrufe anpassen. Dies kann zu einem Fehler führen, wenn Sie einen aktualisieren, aber vergessen, den zu aktualisieren andere (in diesem trivialen Beispiel ist das unwahrscheinlich, aber ich persönlich glaube, dass es eine bessere Praxis für die Wiederverwendung von Code ist).

Wie Stephen Weinberg in seiner Antwort auf diese Frage hervorhebt, müssen Sie die Wartegruppe vor dem Laichen des Gofunc erhöhen. Sie können dies jedoch leicht erreichen, indem Sie den Gofunc-Spawn doSomething()wie folgt in die Funktion selbst einwickeln :

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()
    }()
}

Dann können Sie es ohne goAufruf aufrufen , zB:

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

Als Spielplatz: http://play.golang.org/p/WZcprjpHa_

mroth
quelle
21
  • kleine Verbesserung basierend auf Mroth Antwort
  • Die Verwendung von Defer für Done ist sicherer
  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
quelle