Przykład dla sync.WaitGroup jest poprawne?

108

Czy to przykładowe użycie jest sync.WaitGroupprawidłowe? Daje oczekiwany efekt, ale nie jestem pewien co wg.Add(4)do pozycji i pozycji wg.Done(). Czy ma sens dodawanie jednocześnie czterech gorutyn 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")
}

Wynik (zgodnie z oczekiwaniami):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done
topskip
źródło
1
Co się stanie, jeśli dosomething () ulegnie awarii, zanim będzie można wykonać wg.Done ()?
Mostowski Collapse
19
Zdaję sobie sprawę, że to stare, ale przyszłym ludziom poleciłbym pierwszą defer wg.Done()rozmowę na początku funkcji.
Brian

Odpowiedzi:

154

Tak, ten przykład jest poprawny. Ważne jest, aby wg.Add()zdarzyło się to przed gooświadczeniem, aby zapobiec warunkom wyścigu. Poniższe również byłoby poprawne:

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

Jednak nie ma sensu dzwonić wg.Addw kółko, kiedy już wiesz, ile razy będzie to wywoływane.


Waitgroupspanika, jeśli licznik spadnie poniżej zera. Licznik zaczyna się od zera, każdy Done()jest a -1i każdy Add()zależy od parametru. Tak więc, aby mieć pewność, że licznik nigdy nie spadnie poniżej i unikniesz paniki, musisz Add()mieć pewność, że nastąpi przed Done().

W Go takie gwarancje daje model pamięci .

Model pamięci stwierdza, że ​​wszystkie instrukcje w jednym gorutynie wydają się być wykonywane w tej samej kolejności, w jakiej zostały zapisane. Możliwe, że tak naprawdę nie będą w tej kolejności, ale wynik będzie taki, jakby był. Gwarantuje się również, że gorutyna nie działa, dopóki nie nastąpi gostwierdzenie, które ją woła . Ponieważ wyrażenie Add()występuje przed goinstrukcją, a goinstrukcja występuje przed Done(), wiemy, że Add()występuje przed Done().

Jeśli miałbyś mieć gooświadczenie poprzedzające Add(), program może działać poprawnie. Byłby to jednak warunek wyścigu, ponieważ nie byłby to gwarantowany.

Stephen Weinberg
źródło
9
Mam pytanie dotyczące tego: czy nie byłoby lepiej defer wg.Done(), abyśmy byli pewni, że zostanie wywołany niezależnie od trasy, którą pokona rutynę? Dzięki.
Alessandro Santini
2
jeśli po prostu chciałeś się upewnić, że funkcja nie powróci przed zakończeniem wszystkich procedur go, preferowane byłoby tak odraczanie. po prostu zwykle celem grupy oczekujących jest zaczekanie, aż cała praca zostanie wykonana, a następnie zrobienie czegoś z oczekiwanymi wynikami.
Zanven
1
Jeśli nie używasz deferi jedna z twoich gorutyn nie zadzwoni wg.Done()... czy Waitpo prostu nie zablokujesz się na zawsze? Wygląda na to, że może łatwo wprowadzić trudny do znalezienia błąd do twojego kodu ...
Dan Esparza
29

Zalecałbym wg.Add()wbudowanie wywołania do doSomething()samej funkcji, aby w przypadku dostosowania liczby wywołań nie trzeba było oddzielnie dostosowywać parametru add, co mogłoby prowadzić do błędu, jeśli go zaktualizujesz, ale zapomnisz zaktualizować inne (w tym trywialnym przykładzie jest to mało prawdopodobne, ale osobiście uważam, że jest to lepsza praktyka ponownego wykorzystania kodu).

Jak Stephen Weinberg wskazuje w swej odpowiedzi na to pytanie , trzeba jednak zwiększamy waitgroup przed tarłem na gofunc, ale można to łatwo osiągnąć poprzez owinięcie ikry gofunc wewnątrz doSomething()samej funkcji, jak poniżej:

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

Wtedy możesz to wywołać bez gowywołania, np:

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

Jako plac zabaw: http://play.golang.org/p/WZcprjpHa_

mroth
źródło
21
  • mała poprawa w oparciu o odpowiedź Mrotha
  • używanie odroczenia na Gotowe jest bezpieczniejsze
  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
źródło