示例对sync.WaitGroup正确吗?


108

此示例用法sync.WaitGroup正确吗?它给出了预期的结果,但是我不确定wg.Add(4)的位置wg.Done()。一次添加四个goroutine是否有意义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

19
我意识到这已经很老了,但是对于将来的人们,我建议defer wg.Done()在函数开始时进行一次初始调用。
布赖恩

Answers:


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中,这种保证由内存模型给出

内存模型指出,单个goroutine中的所有语句似乎都按照与编写时相同的顺序执行。他们可能实际上并不会按此顺序排列,但结果将好像是这样。还可以确保goroutine直到go调用它的语句之后才运行。由于Add()发生在go语句之前,且go发生在语句之前Done(),我们知道Add()发生在语句之前Done()

如果要让该go语句位于之前Add(),则该程序可能会正确运行。但是,这将是一种竞赛条件,因为无法保证。


9
我对此有一个疑问:这样做会更好defer wg.Done()吗,这样我们可以确保无论goroutine采取什么路线,都可以调用它?谢谢。
亚历山德罗·桑蒂尼

2
如果您纯粹是想确保函数在所有go例程完成之前没有返回,则可以使用defer。通常,等待组的整个重点是等待所有工作完成,然后对您等待的结果进行处理。
Zanven

1
如果您不使用defer并且您的goroutine之一无法调用wg.Done()...您将不会Wait永远被永久阻止?听起来很容易在您的代码中引入难以发现的错误……
Dan Esparza

29

我建议将wg.Add()调用嵌入到doSomething()函数本身中,这样,如果您调整调用的次数,则不必手动单独调整add参数,如果更新一个参数却忘记更新该参数,可能会导致错误。其他(在这个琐碎的示例中,这不太可能,但我个人仍然认为这是重复使用代码的更好做法)。

正如史蒂芬·温伯格(Stephen Weinberg)在对这个问题的回答中指出的那样,您确实必须在生成gofunc 之前增加waitgroup,但是您可以通过将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
  • 基于Mroth答案的小改进
  • 使用defer进行更安全
  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")
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.