Exemplo para sync.WaitGroup correto?

108

Este exemplo de uso é sync.WaitGroupcorreto? Ele dá o resultado esperado, mas não tenho certeza sobre a wg.Add(4)e a posição de wg.Done(). Faz sentido adicionar os quatro goroutines de uma vez com 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")
}

Resultado (conforme esperado):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done
pular
fonte
1
E se dosomething () travar antes de fazer wg.Done ()?
Mostowski Collapse
19
Sei que isso é antigo, mas para quem quer que seja, recomendo uma defer wg.Done()chamada inicial no início da função.
Brian

Respostas:

154

Sim, este exemplo está correto. É importante que wg.Add()aconteça antes da godeclaração para evitar condições de corrida. O seguinte também seria correto:

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

No entanto, é inútil ligar wg.Addrepetidamente quando você já sabe quantas vezes ela será chamada.


Waitgroupsentre em pânico se o contador cair abaixo de zero. O contador começa em zero, cada Done()um é a -1e cada um Add()depende do parâmetro. Portanto, para garantir que o contador nunca caia abaixo e evitar pânico, você precisa Add()ter a garantia de vir antes do Done().

No Go, essas garantias são dadas pelo modelo de memória .

O modelo de memória afirma que todas as instruções em uma única goroutine parecem ser executadas na mesma ordem em que são escritas. É possível que eles não estejam realmente nessa ordem, mas o resultado será como se estivesse. Também é garantido que um goroutine não será executado até depois da goinstrução que o chama . Como o Add()ocorre antes da goinstrução e a goinstrução ocorre antes de Done(), sabemos que Add()ocorre antes de Done().

Se você deseja que a goinstrução venha antes de Add(), o programa pode funcionar corretamente. No entanto, seria uma condição de corrida porque não seria garantida.

Stephen Weinberg
fonte
9
Eu tenho uma pergunta sobre este: não seria melhor para defer wg.Done()termos certeza de que ele será chamado independentemente da rota que o goroutine tomar? Obrigado.
Alessandro Santini
2
se você simplesmente quisesse ter certeza de que a função não retornou antes de todas as rotinas go serem concluídas, sim adiar seria o preferido. geralmente, o objetivo de um grupo de espera é esperar até que todo o trabalho seja concluído para, então, fazer algo com os resultados que você esperava.
Zanven
1
Se você não usar defere um de seus goroutines não ligar wg.Done()... você não vai Waitbloquear para sempre? Parece que pode facilmente introduzir um bug difícil de encontrar em seu código ...
Dan Esparza
29

Eu recomendaria incorporar a wg.Add()chamada à doSomething()própria função, de modo que se você ajustar o número de vezes que ela é chamada, não precise ajustar separadamente o parâmetro add manualmente, o que pode levar a um erro se você atualizar um, mas se esquecer de atualizar o outro (neste exemplo trivial isso é improvável, mas ainda assim, eu pessoalmente acredito que seja uma prática melhor para reutilização de código).

Como Stephen Weinberg aponta em sua resposta a esta pergunta , você tem que incrementar o grupo de espera antes de gerar o gofunc, mas você pode fazer isso facilmente envolvendo o spawn do gofunc dentro da doSomething()própria função, assim:

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

Então você pode chamá-lo sem a goinvocação, por exemplo:

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

Como um playground: http://play.golang.org/p/WZcprjpHa_

maroth
fonte
21
  • pequena melhoria com base na resposta de Mroth
  • usar adiar para Concluído é mais seguro
  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
fonte