理解真實世界的併發Bug

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創建了一個子協程來執行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運行時選擇了超時分支(非確定性),子協程就會永遠阻塞。

Go併發模式使用情況

這一節分析了6個項目裏goroutine、併發原語的使用情況。

goroutine使用情況

匿名函數的goroutine使用比普通函數要多,基本每1~5千行代碼創建一個goroutine。

併發原語

雖然Go鼓勵消息傳遞,但是在這些大項目裏,共享內存的使用比消息傳遞要多,Mutex基本在channel的兩倍以上。

Bug分類

這篇論文裏,按兩個維度對bug進行分類:

  1. 行爲:阻塞和非阻塞,阻塞bug指goroutine意外地阻塞無法繼續執行的情況(例如死鎖),非阻塞bug通常是數據衝突
  2. 原因:共享內存和消息傳遞,因爲用了這兩種技術之一導致的bug

clipboard.png

可以看到,共享內存其實導致了更多的bug。

阻塞bug

clipboard.png

消息傳遞和共享內存導致的阻塞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

clipboard.png

共享內存導致更多的非阻塞bug,幾乎是消息傳遞的8倍。

例如在下面這段代碼裏,每當ticker觸發時執行一次f(),通過stopCh退出循環:

ticker := time.NewTicker()
for {
    f()
    select {
        case <- stopCh
            return
        case <- ticker
    }
}

但是select是非確定性的,stopChticker同時發生時,不一定會執行stopChan的分支,正確做法是先檢查一次stopCh

ticker := time.NewTicker()
for {
    select{
        case <- stopCh:
            return
        default:
    }
    f()
    select {
        case <- stopCh:
            return
        case <- ticker:
    }
}

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章