等待不确定数量的goroutine

等待不确定数量的goroutine,go,Go,我有一个代码,其中一个goroutine将触发不确定数量的子goroutine,而子goroutine又将触发更多goroutine,等等。我的目标是等待所有子goroutine完成 我不知道我将提前触发的goroutine的总数,因此我不能使用,理想情况下,我不必人为地限制通过该模式运行的goroutine的总数 简单地说,我想过在每个goroutine中都有一个本地通道或waitgroup,作为一个信号量来等待其所有子节点,但这会导致每个goroutine在其所有decentant完成时都会

我有一个代码,其中一个goroutine将触发不确定数量的子goroutine,而子goroutine又将触发更多goroutine,等等。我的目标是等待所有子goroutine完成

我不知道我将提前触发的goroutine的总数,因此我不能使用,理想情况下,我不必人为地限制通过该模式运行的goroutine的总数

简单地说,我想过在每个goroutine中都有一个本地通道或waitgroup,作为一个信号量来等待其所有子节点,但这会导致每个goroutine在其所有decentant完成时都会占用堆栈空间

现在我的想法是在goroutine启动时增加一个值(在父进程中,如果子进程在父进程完成后开始运行,则避免错误地命中零),在goroutine完成时减小它,并定期检查它是否等于零

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

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

wg.Add(1)
每一个goroutine都把

defer wg.Done()
作为第一句话。现在你可以做了

wg.Wait

等待所有的goroutine完成。

我编写了
sync.WaitGroup
的第一个实现,这个和其他边缘案例都得到了很好的支持。从那时起,Dmitry改进了实现,考虑到他的过往记录,我打赌他只会让它更安全

特别是,您可以相信,如果当前有一个或多个被阻止的
等待
调用,然后在调用
完成
之前以正增量调用
添加
,则不会取消阻止以前存在的任何
等待
调用

因此,您完全可以这样做,例如:

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
在与
G3
竞速时也在执行
Add(1)+go f()
模式

不过,我理解你的问题,因为最近放在那里的文档中确实有一个令人困惑的陈述,但让我们看看评论的历史,看看它实际上在解决什么问题

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

这是真的打破了。评论应该改进得更具体一些,避免你在阅读时产生的似是而非但误导性的理解。

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

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

// 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
func1:=func(){
对于char:='a';char<'a'+3;char++{
fmt.Printf(“%c”,字符)
}
}
func2:=func(){
对于数字:=1;数字<4;数字++{
fmt.Printf(“%d”,编号)
}
}
func3:=func(){
并行化(func1、func2)
}
并行化(func3,func3)//a 1 b 2 c 3 3

如果您想使用它,可以在这里找到它。

WaitGroup的文档说明:“请注意,具有正增量的调用必须在调用Wait之前发生,否则Wait可能会等待太小的组。通常这意味着要添加的调用应该在创建goroutine或其他要等待的事件的语句之前执行。”这给我的印象是,如果我在父母的“兄弟姐妹”中等待,我不应该在孩子中调用Add。@Bryce:如果孩子本身创建了更多的gorutine,那么在相关的
go
语句之前,你应该调用'Add'。Just.addbefore
go
,并确保任何级别的父goroutine在其返回之前执行该操作。这样可以确保对
WaitGroup
的每次“添加”都发生在其关联的“完成”之前。IINM;-)“添加”和“完成”的顺序是正确的,但我对文档的理解是,你所有的“添加”都应该在你尝试“等待”之前发生,这是我无法保证的——除非我在每个goroutine中为它的孩子“等待”,这导致我所有的孩子都在附近闲逛,而不是把他们的孩子送回去。@布莱斯:啊,我从来没有注意到这一点。那么,
WaitGroup
是一个坏的、无用的东西,至少在这种情况下是这样,你必须自己编写替换代码。对不起,吵闹了。稍后将删除我的答案。与其将其删除,不如在顶部添加一条评论,说“这实际上不太管用——请参见评论”。这引发了一些很好的讨论,不会帮助任何人将其全部删除。