Ejemplo de sync.WaitGroup correcto?

108

¿Este ejemplo de uso es sync.WaitGroupcorrecto? Da el resultado esperado, pero no estoy seguro sobre el wg.Add(4)y la posición de wg.Done(). ¿Tiene sentido agregar las cuatro gorutinas a la vez 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 (como se esperaba):

Function in background, duration: 150ms
Function in background, duration: 200ms
Function in background, duration: 400ms
Function in background, duration: 600ms
Done
topskip
fuente
1
¿Qué pasa si dosomething () falla antes de que pueda hacer wg.Done ()?
Mostowski Collapse
19
Me doy cuenta de que esto es antiguo, pero para las personas futuras, recomendaría una defer wg.Done()llamada inicial al inicio de la función.
Brian

Respuestas:

154

Sí, este ejemplo es correcto. Es importante que wg.Add()ocurra antes de la godeclaración para evitar condiciones de carrera. Lo siguiente también sería correcto:

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

Sin embargo, no tiene sentido llamar wg.Adduna y otra vez cuando ya sabe cuántas veces se llamará.


Waitgroupspánico si el contador cae por debajo de cero. El contador comienza en cero, cada uno Done()es un -1y cada uno Add()depende del parámetro. Por lo tanto, para asegurarse de que el mostrador nunca caiga por debajo y evitar el pánico, debe Add()tener la garantía de llegar antes que el Done().

En Go, estas garantías vienen dadas por el modelo de memoria .

El modelo de memoria establece que todas las declaraciones en una sola goroutine parecen ejecutarse en el mismo orden en que están escritas. Es posible que en realidad no estén en ese orden, pero el resultado será como si lo fuera. También se garantiza que una goroutine no se ejecuta hasta después de la godeclaración que la llama . Dado que Add()ocurre antes del goenunciado y el goenunciado ocurre antes del Done(), sabemos que Add()ocurre antes del Done().

Si tuviera la godeclaración antes del Add(), el programa puede funcionar correctamente. Sin embargo, sería una condición de carrera porque no estaría garantizada.

Stephen Weinberg
fuente
9
Tengo una pregunta sobre esta: ¿no sería mejor para defer wg.Done()estar seguros de que se llama independientemente de la ruta que tome la goroutine? Gracias.
Alessandro Santini
2
si simplemente desea asegurarse de que la función no regrese antes de que se terminen todas las rutinas, entonces se preferiría yes defer. por lo general, el objetivo de un grupo de espera es esperar hasta que todo el trabajo esté terminado para luego hacer algo con los resultados que estaba esperando.
Zanven
1
Si no lo usa defery una de sus goroutines no llama wg.Done()... ¿no Waitse bloqueará simplemente para siempre? Esto parece que podría introducir fácilmente un error difícil de encontrar en su código ...
Dan Esparza
29

Recomendaría incrustar la wg.Add()llamada en la doSomething()función en sí, de modo que si ajusta el número de veces que se llama, no tiene que ajustar por separado el parámetro de adición manualmente, lo que podría provocar un error si actualiza uno pero se olvida de actualizar el otro (en este ejemplo trivial eso es poco probable, pero aún así, personalmente creo que es una mejor práctica para la reutilización del código).

Como señala Stephen Weinberg en su respuesta a esta pregunta , debe incrementar el grupo de espera antes de generar el gofunc, pero puede lograr esto fácilmente envolviendo el spawn de gofunc dentro de la doSomething()función en sí, así:

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

Entonces puede llamarlo sin la goinvocación, por ejemplo:

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

Como parque infantil: http://play.golang.org/p/WZcprjpHa_

polilla
fuente
21
  • pequeña mejora basada en la respuesta de Mroth
  • usar aplazar para Listo es más 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
fuente