等待不确定数量的goroutines

时间:2013-09-14 19:10:08

标签: go

我有一个代码,其中一个goroutine将触发不确定数量的儿童goroutine,这反过来将触发更多goroutines等。我的目标是等待所有的儿童goroutines完成。

我不知道我将提前解雇的goroutines的总数,所以我不能使用sync.WaitGroup,理想情况下我不必人为地限制通过{运行的goroutines总数{3}}模式。

简单地说,我想在每个goroutine中都有一个本地频道或等待组作为等待其所有子节点的信号量,但这会导致每个goroutine在所有后代完成时都会消耗堆栈空间。

现在我的想法是在goroutine被触发时增加channel-as-semaphore(在父节点中,以避免在父节点完成后子节点开始运行时虚假地击中零),在goroutine结束时减少它,并定期检查它是否等于零。

我基本上是在正确的轨道上,还是有更优雅的解决方案?

3 个答案:

答案 0 :(得分:10)

我编写了sync.WaitGroup的第一个实现,并且这个和其他边缘情况得到了很好的支持。从那以后,德米特里改进了实施,并且根据他的记录,我敢打赌他只是让它更安全。

特别是,如果当前有一个或多个被阻止的Wait来电,您可以相信,然后在拨打Add之前拨打正数增量Done,那么您就赢了解锁任何以前存在的Wait个电话。

所以你绝对可以这样做,例如:

var wg sync.WaitGroup
wg.Add(1)
go func() {
    wg.Add(1)
    go func() {
        wg.Done()
    }()
    wg.Done()
}()
wg.Wait()

自从代码首次集成以来,我实际上在生产中使用了等效的逻辑。

作为参考,这个内部评论在第一个实现中已经存在,并且仍然存在:

// WaitGroup creates a new semaphore each time the old semaphore
// is released. This is to avoid the following race:
//
// G1: Add(1)
// G1: go G2()
// G1: Wait() // Context switch after Unlock() and before Semacquire().
// G2: Done() // Release semaphore: sema == 1, waiters == 0. G1 doesn't run yet.
// G3: Wait() // Finds counter == 0, waiters == 0, doesn't block.
// G3: Add(1) // Makes counter == 1, waiters == 0.
// G3: go G4()
// G3: Wait() // G1 still hasn't run, G3 finds sema == 1, unblocked! Bug.

这描述了在触及实现时要记住的不同竞争条件,但请注意,即使G1 Add(1) + go f()模式与G3竞争时也是如此。

我理解你的问题,因为最近在文档中确实存在令人困惑的陈述,但让我们看看评论的历史,看看它实际上是在解决什么。

评论由Russ提出,修订版15683:

(...)
+// Note that calls with positive delta must happen before the call to Wait,
+// or else Wait may wait for too small a group. Typically this means the calls
+// to Add should execute before the statement creating the goroutine or
+// other event to be waited for. See the WaitGroup example.
func (wg *WaitGroup) Add(delta int) {

Russ的日志评论:

  

同步:添加关于调用位置的警告(* WaitGroup)。添加

     

修正问题4762。

如果我们阅读第4762期,我们会发现:

  

在sync.WaitGroup的文档中添加一个明确的注释可能值得在启动包含对Done调用的go例程之前完成对Add的调用。

因此,文档实际上是针对这样的代码发出警告:

var wg sync.WaitGroup
wg.Add(1)
go func() {
    go func() {
        wg.Add(1)
        wg.Done()
    }()
    wg.Done()
}()
wg.Wait()

这确实被打破了。评论应该进一步改进,以便更加具体,避免在阅读时你理解但误导的理解。

答案 1 :(得分:0)

当然你可以使用sync.WaitGroup来完成你的任务,它实际上是一个完美的契合,专为此而设计。您要创建的goroutine的数量是不确定。它只是在运行时才知道的值,然后它就是完全。每个go语句都会创建一个新的goroutine。 之前这样的go语句,无论执行多少次,都必须执行

wg.Add(1)

insdide 每个goroutine put

defer wg.Done()

作为第一个声明。现在你可以做到

wg.Wait

等待所有你的goroutines完成。

答案 2 :(得分:0)

我喜欢WaitGroup的简单。关于WaitGroup,我不喜欢的一件事就是必须在你的goroutine中传递对它的引用,因为你会将并发逻辑与业务逻辑混合在一起。此外,在您的情况下,如果您不小心,它可能会变得更加复杂和容易出错。

所以我想出了这个通用函数来解决这个问题:

// Parallelize parallelizes the function calls
func Parallelize(functions ...func()) {
    var waitGroup sync.WaitGroup
    waitGroup.Add(len(functions))

    defer waitGroup.Wait()

    for _, function := range functions {
        go func(copy func()) {
            defer waitGroup.Done()
            copy()
        }(function)
    }
}

以下是我将如何使用它来解决您的问题:

func1 := func() {
        for char := 'a'; char < 'a' + 3; char++ {
            fmt.Printf("%c ", char)
        }
}

func2 := func() {
        for number := 1; number < 4; number++ {
            fmt.Printf("%d ", number)
        }
}

func3 := func() {
        Parallelize(func1, func2)
}

Parallelize(func3, func3)  // a a 1 1 b b 2 2 c c 3 3

如果您想使用它,可以在https://github.com/shomali11/util

找到它