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: } }