Go带来了新的并发原语和并发模式(其实也不太新),若是没有深刻了解这些特性,同样会写出并发bug。git
在 Understanding Real-World Concurrency Bugs in Go 这篇论文里,做者系统地分析了6个流行的Go项目(Docker、Kubernetes、gRPC-go、etcd、CockroachDB、 BoltD)和其中171个并发bug,经过这些分析咱们能够加深对Go的并发模型的理解,从而产出更好、更可靠的代码。github
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
建立了一个子协程来执行fn
而后经过select
等待子协程完成或超时:并发
func finishReq(timeout time.Duration) r ob { ch :=make(chanob) // ch :=make(chanob, 1) // 修复方案 go func() { result := fn() ch <- result // 阻塞 } select { case result = <- ch return result case <- time.After(timeout) return nil } } }
若是超时先发生,或者子协程和超时同时发生但go运行时选择了超时分支(非肯定性),子协程就会永远阻塞。函数
这一节分析了6个项目里goroutine、并发原语的使用状况。学习
匿名函数的goroutine使用比普通函数要多,基本每1~5千行代码建立一个goroutine。spa
虽然Go鼓励消息传递,可是在这些大项目里,共享内存的使用比消息传递要多,Mutex基本在channel的两倍以上。code
这篇论文里,按两个维度对bug进行分类:协程
能够看到,共享内存其实致使了更多的bug。ip
消息传递和共享内存致使的阻塞bug几乎同样多,并且消息传递的阻塞bug都和Go的消息传递语义例如channel有关,消息传递和共享内存一块儿使用的时候会很难发现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,几乎是消息传递的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: } }