go - 一个'所有goroutines都睡着了-死锁!'的例子我不知道为什么
问题描述
TL;DR:一个典型的all goroutines are asleep, deadlock!
但无法弄清楚的案例
我正在解析维基词典 XML 转储以构建单词数据库。我将每篇文章的文本解析推迟到一个 goroutine 中,希望它能加快这个过程。
它是 7GB,在我的机器上连续处理时不到 2 分钟,但如果我可以利用所有内核,为什么不呢。
一般来说,我是线程新手,我遇到了一个all goroutines are asleep, deadlock!
错误。
这里有什么问题?
这可能根本没有性能,因为它使用无缓冲的通道,所以所有的 goroutine 最终都会有效地串行执行,但我的想法是学习和理解线程,并用不同的替代方案来衡量它需要多长时间:
- 无缓冲通道
- 不同大小的缓冲通道
- 一次只调用尽可能多的 goroutine
runtime.NumCPU()
我的伪代码代码摘要:
while tag := xml.getNextTag() {
wg.Add(1)
go parseTagText(chan, wg, tag.text)
// consume a channel message if available
select {
case msg := <-chan:
// do something with msg
default:
}
}
// reading tags finished, wait for running goroutines, consume what's left on the channel
for msg := range chan {
// do something with msg
}
// Sometimes this point is never reached, I get a deadlock
wg.Wait()
----
func parseTagText(chan, wg, tag.text) {
defer wg.Done()
// parse tag.text
chan <- whatever // just inform that the text has been parsed
}
完整代码: https:
//play.golang.org/p/0t2EqptJBXE
解决方案
在 Go Playground 上的完整示例中,您:
创建一个通道(第 39 行
results := make(chan langs)
)和一个等待组(第 40 行var wait sync.WaitGroup
)。到目前为止,一切都很好。循环:在循环中,有时会衍生出一个任务:
if ...various conditions... { wait.Add(1) go parseTerm(results, &wait, text) }
在循环中,有时会从通道进行非阻塞读取(如您的问题所示)。这里也没有问题。但...
在循环结束时,使用:
for res := range results { ... }
在所有作家完成之后,从来没有
close(results)
在一个地方打电话。此循环使用来自通道的阻塞读取。只要某个 writer goroutine 仍在运行,阻塞读取就可以在不让整个系统停止的情况下阻塞,但是当最后一个 writer 完成写入并退出时,就没有剩余的 writer goroutines。任何其他剩余的 goroutine 可能会拯救你,但没有。
由于您使用var wait
正确(在正确的位置添加 1,并在编写Done()
器的正确位置调用),解决方案是再添加一个 goroutine,这将是拯救您的那个:
go func() {
wait.Wait()
close(results)
}()
for res := range results
你应该在进入循环之前分离这个救援者协程。(如果您更早地将其分拆,它可能会看到wait
变量过早地倒数到零,就在它通过分拆另一个 重新计数之前parseTerm
。)
这个匿名函数将阻塞在wait
变量的Wait()
函数中,直到最后一个编写者 goroutine 调用了 final wait.Done()
,这将解除阻塞这个goroutine。然后这个 goroutine 将调用close(results)
,这将安排for
你的 goroutine 中的循环main
完成,解除阻塞该 goroutine。当这个 goroutine(救援者)返回并因此终止时,不再有救援者,但我们不再需要任何救援者。
wait.Wait()
(然后这个主代码不必要地调用:因为for
直到新wait.Wait()
的 goroutine中的已经解除阻塞才终止,我们知道这个 next将立即返回。所以我们可以放弃第二个调用,尽管把它留在里面是无害的。)wait.Wait()