Пример для sync.WaitGroup правильно?

108

Правильно ли этот пример использования sync.WaitGroup? Это дает ожидаемый результат, но я не уверен насчет положения wg.Add(4)и wg.Done(). Имеет ли смысл добавлять сразу четыре горутины 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")
}

Результат (как и ожидалось):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done
топскип
источник
1
Что если dosomething () выйдет из строя раньше, чем сможет выполнить wg.Done ()?
Mostowski Collapse
19
Я понимаю, что это устарело, но для будущих людей я бы порекомендовал выполнить первоначальный defer wg.Done()вызов в начале функции.
Брайан

Ответы:

154

Да, этот пример верен. Важно, чтобы это произошло wg.Add()перед goоператором, чтобы предотвратить состояние гонки. Также будет правильным следующее:

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

Однако звонить wg.Addснова и снова, когда вы уже знаете, сколько раз он будет вызван, довольно бессмысленно .


Waitgroupsпаника, если счетчик упадет ниже нуля. Счетчик начинается с нуля, каждый Done()равен a, -1и каждый Add()зависит от параметра. Итак, чтобы счетчик никогда не опускался ниже, и чтобы избежать паники, вам нужно, Add()чтобы он гарантированно пришел раньше Done().

В Go такие гарантии дает модель памяти .

Модель памяти утверждает, что все операторы в одной горутине выполняются в том же порядке, в котором они написаны. Возможно, они на самом деле не будут в таком порядке, но результат будет таким, как если бы он был. Также гарантируется, что горутина не запустится до тех пор, пока не будет вызвана goинструкция, которая ее вызывает . Поскольку оператор Add()встречается перед goоператором, а goоператор - перед оператором Done(), мы знаем, что это Add()происходит перед оператором Done().

Если вы поставили этот goоператор перед Add(), программа может работать правильно. Однако это будет состояние гонки, потому что это не будет гарантировано.

Стивен Вайнберг
источник
9
У меня есть вопрос по этому поводу: не было бы лучше, чтобы defer wg.Done()мы были уверены, что он будет вызван независимо от маршрута, по которому идет горутина? Спасибо.
Алессандро Сантини,
2
если вы просто хотите убедиться, что функция не вернется до того, как все подпрограммы go будут завершены, то предпочтительнее будет да, defer. просто обычно весь смысл группы ожидания состоит в том, чтобы дождаться завершения всей работы, чтобы затем что-то сделать с результатами, которых вы ждали.
Zanven
1
Если вы не используете, deferи одна из ваших горутин не запускается wg.Done()... не будет ли ваша Waitпросто заблокирована навсегда? Похоже, это может легко внести в ваш код труднообнаружимую ошибку ...
Дэн Эспарза
29

Я бы порекомендовал встроить wg.Add()вызов в doSomething()саму функцию, чтобы, если вы измените количество раз, которое он вызывается , вам не нужно отдельно настраивать параметр добавления вручную, что может привести к ошибке, если вы обновите один, но забудете обновить другое (в этом тривиальном примере это маловероятно, но все же я лично считаю, что это лучшая практика для повторного использования кода).

Как указывает Стивен Вайнберг в своем ответе на этот вопрос , вам нужно увеличить группу ожидания до создания gofunc, но вы можете легко добиться этого, заключив порождение gofunc внутри самой doSomething()функции, например:

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

Затем вы можете вызвать его без goвызова, например:

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

В качестве игровой площадки: http://play.golang.org/p/WZcprjpHa_

мрот
источник
21
  • небольшое улучшение, основанное на ответе Мрота
  • использование defer for Done безопаснее
  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")
}
Бная
источник