Go带来了新的并发原语和并发模式(其实也不太新),如果没有深入了解这些特性,一样会写出并发bug。
在 Understanding Real-World Concurrency Bugs in Go 这篇论文里,作者系统地分析了6个流行的Go项目(Docker、Kubernetes、gRPC-go、etcd、CockroachDB、 BoltD)和其中171个并发bug,通过这些分析我们可以加深对Go的并发模型的理解,从而产出更好、更可靠的代码。
Our study shows that it is as easy to make concurrency bugs with message passing as with shared memory,sometimes even more.
我们的研究表明,消息传递和共享内存一样、有时甚至更容易写出并发错误。
例如下面是k8s的一个bug,finishReq
创建了一个 goroutine 来执行fn
然后通过 select
等待 goroutine 完成或超时:
func finishReq(timeout time.Duration) r ob {
ch :=make(chan ob)
// ch :=make(chan ob, 1) // 修复方案
go func() {
result := fn()
ch <- result // 阻塞
}
select {
case result := <- ch:
return result
case <- time.After(timeout)
return nil // 如果执行到这里,不会再从 ch 读数据,上面 goroutine 永远阻塞
}
}
如果超时先发生,或者 goroutine 写 channel 和超时同时发生但go运行时选择了超时分支,goroutine 会永远阻塞导致 goroutine 泄漏。
Go并发模式使用情况
这一节分析了6个项目里 goroutine 和并发原语的使用情况。
匿名函数的 goroutine 使用比普通函数要多,基本每 2~5 千行代码创建一个 goroutine:
虽然 Go 鼓励消息传递,但是在这些大项目里,共享内存的使用比消息传递要多,Mutex 基本在 channel 的两倍以上:
Bug分类
这篇论文里,按两个维度对 bug 进行分类:
- 行为:阻塞和非阻塞,阻塞 bug 指 goroutine 意外地阻塞无法继续执行的情况(例如死锁),非阻塞 bug 通常是数据冲突(例如并发读写)
- 原因:共享内存和消息传递,因为用了这两种技术之一导致的bug
数量上,共享内存有的更多 bug,但是考虑到共享内存的使用比消息传递多(见上一节的统计),用的多自然 bug 也多,所以平均下相差不大。
阻塞bug
消息传递产生了更多的阻塞 bug,特保是和共享内存一起使用的时候,产生的 bug 很不好发现。
共享内存阻塞 bug 实例,Docker 错误使用 WaitGroup
:
var group sync.WaitGroup
group.Add(len(pm.plugins))
for _, p := range pm.plugins {
go func(p *plugin) {
defer group.Done()
}
group.Wait() // 阻塞
}
// 应该在这里group.Wait()
错误使用 channel 和 mutex 导致阻塞:
func goroutine1() {
m.Lock()
ch <- request // 阻塞
m.Unlock()
}
func goroutine2() {
for {
m.Lock() // 阻塞
m.Unlock()
request <- ch
}
}
非阻塞bug
共享内存导致更多的非阻塞 bug,几乎是消息传递的8倍。
例如在下面这段代码里,每当ticker
触发时执行一次f()
,通过stopCh
退出循环:
ticker := time.NewTicker()
for {
f()
select {
case <- stopCh:
return
case <- ticker:
}
}
但是select是非确定性的,stopCh
和ticker
同时发生时,不一定会执行stopChan
的分支,正确做法是先检查一次stopCh
:
ticker := time.NewTicker()
for {
select { // 先检查一次
case <- stopCh:
return
default:
}
f()
select {
case <- stopCh:
return
case <- ticker:
}
}
参考
- system-pclub/go-concurrency-bugs:论文数据集,包含各项目真实bug代码和修复,是个非常好的学习资源。
- Understanding Real-World Concurrency Bugs in Go:论文本体
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。