带有sync.WaitGroup的Goroutines在最后一个wg.Done()之前结束

时间:2017-04-02 09:28:06

标签: go goroutine

我有一个示例代码(您可以在Go Playground上找到它):

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    messages := make(chan int)
    var wg sync.WaitGroup
    var result []int

    // you can also add these one at 
    // a time if you need to 

    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 1)
        messages <- 1
    }()
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 1)
        messages <- 2
    }() 
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 1)
        messages <- 3
    }()
    go func() {
        for i := range messages {
            fmt.Println(i)
        result = append(result, i)
        }

    }()

    wg.Wait()
    fmt.Println(result)
}

我得到了这个输出:

2
1
[2 1]

我想我知道为什么会这样,但我无法解决。 WaitGroup中有3个项目我指的是三个goroutine,第四个groutine消耗来自通道的数据。当最后一个灌浆说wg.Done()时程序结束了,因为wg.Wait()表示每个goroutine都完成了,最后的goroutine结果是第4个goroutine无法消耗,因为程序结束了。我尝试在第4个函数中添加带有wg.Add(1)和wg.Done()的加号,但在这种情况下我遇到了死锁。

3 个答案:

答案 0 :(得分:4)

你产生的最后一个goroutine - 那个打算收集结果的goroutine - 不会被main()等待,因此返回wg.Wait()main()退出并获得剩余的goroutines。 据说当时只剩下一个收集goroutine但它无法更新切片。

另请注意,由于您在程序中进行数据竞争的原因相同:在main()读取结果片段时,它 可以安全地阅读它 - 也就是说,作者是否完成了写作。

一个简单的解决方法是为该goroutine添加wg.Add(1),并在其中添加defer wg.Done()

更好的解决方案是在close()之后messages wg.Wait()频道 并且在从切片读取之前。这将使收集goroutine的range循环终止,这也将在goroutine和main()之间创建一个正确的同步点。

答案 1 :(得分:2)

关闭频道是用于信令的惯用Go模式,如果您关闭缓冲频道,则消费者可以读取所有排队的数据然后停止。

此代码可正常运行:

func main() {
    messages := make(chan int)
    var wg sync.WaitGroup
    var result []int

    // you can also add these one at
    // a time if you need to

    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 1)
        messages <- 1
    }()
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 1)
        messages <- 2
    }()
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(time.Second * 1)
        messages <- 3
    }()

    // this goroutine added to signal end of data stream
    // by closing messages channel
    go func() {
        wg.Wait()
        close(messages)
    }()

    // if you need this to happen inside a go routine,
    // this channel is used for signaling end of the work,
    // also another sync.WaitGroup could be used, but for just one
    // goroutine, a single channle as a signal makes sense (there is no
    // groups)
    done := make(chan struct{})
    go func() {
        defer close(done)
        for i := range messages {
            fmt.Println(i)
            result = append(result, i)
        }
    }()

    <-done
    fmt.Println(result)
}

如您所见,我们刚刚添加了另一个关闭messages频道的goroutine,当所有制作人都完成时。

答案 2 :(得分:0)

kostix 的答案是正确的,直到他们提到

  

一个简单的解决方法是为该goroutine添加wg.Add(1)并在其中延迟wg.Done()

这将导致您的循环在不关闭消息通道的情况下永远无法完成!因此,主goroutine将在您的最后一个“收集” goroutine完成之前再次完成。如果将goroutine绑定到wg WaitGroup,它将永远不会发送Done()信号,也会导致错误。

然后当他们提到

  

更好的解决方案是在close()之后且从切片读取之前,wg.Wait()个消息通道

他们建议的放置位置将再次给您同样的错误,因为您将在同一WaitGroup wg上等待。在您上一个“正在收集”的goroutine会继续在您的messages频道中寻找更多消息,并且永远不会到达延迟的wg.Done()

然后 Alex Yu 的注释通过在完全读取结果之前等待来解决它,这是一个很好的解决方法。但是,如果您希望收集的goroutine立即开始,而不是等待所有以前的goroutine(写入messages通道)在它开始从所述通道读取之前完成,那么我建议您...

创建一个结果WaitGroup Add(1),然后再开始最后一个“收集”程序中的defer wgResult.Done(),然后最后在wg.Wait()和{ {1}},您应该fmt.Println(result)close(messages)

这允许您所有的go例程尽快启动,并且仅在编写goroutine和阅读goroutine时需要等待。

下面是带有建议解决方案的GoPlayground链接

https://play.golang.org/p/na0JS1HTwNP