等待不确定数量的goroutine
我有一个代码,其中一个goroutine将触发不确定数量的子goroutine,而子goroutine又将触发更多goroutine,等等。我的目标是等待所有子goroutine完成 我不知道我将提前触发的goroutine的总数,因此我不能使用,理想情况下,我不必人为地限制通过该模式运行的goroutine的总数 简单地说,我想过在每个goroutine中都有一个本地通道或waitgroup,作为一个信号量来等待其所有子节点,但这会导致每个goroutine在其所有decentant完成时都会占用堆栈空间 现在我的想法是在goroutine启动时增加一个值(在父进程中,如果子进程在父进程完成后开始运行,则避免错误地命中零),在goroutine完成时减小它,并定期检查它是否等于零 我基本上是在正确的轨道上,还是有更优雅的解决方案?当然你可以使用等待不确定数量的goroutine,go,Go,我有一个代码,其中一个goroutine将触发不确定数量的子goroutine,而子goroutine又将触发更多goroutine,等等。我的目标是等待所有子goroutine完成 我不知道我将提前触发的goroutine的总数,因此我不能使用,理想情况下,我不必人为地限制通过该模式运行的goroutine的总数 简单地说,我想过在每个goroutine中都有一个本地通道或waitgroup,作为一个信号量来等待其所有子节点,但这会导致每个goroutine在其所有decentant完成时都会
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.addbeforego
,并确保任何级别的父goroutine在其返回之前执行该操作。这样可以确保对WaitGroup
的每次“添加”都发生在其关联的“完成”之前。IINM;-)“添加”和“完成”的顺序是正确的,但我对文档的理解是,你所有的“添加”都应该在你尝试“等待”之前发生,这是我无法保证的——除非我在每个goroutine中为它的孩子“等待”,这导致我所有的孩子都在附近闲逛,而不是把他们的孩子送回去。@布莱斯:啊,我从来没有注意到这一点。那么,WaitGroup
是一个坏的、无用的东西,至少在这种情况下是这样,你必须自己编写替换代码。对不起,吵闹了。稍后将删除我的答案。与其将其删除,不如在顶部添加一条评论,说“这实际上不太管用——请参见评论”。这引发了一些很好的讨论,不会帮助任何人将其全部删除。