现实世界充满了bug_SegmentFault 思否

论坛 期权论坛 编程之家     
选择匿名的用户   2021-6-2 01:06   87   0

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

}

select {

case

result =

return result

case

return nil

}

}

}

如果超时先发生,或者子协程和超时同时发生但go运行时选择了超时分支(非确定性),子协程就会永远阻塞。

Go并发模式使用情况

这一节分析了6个项目里goroutine、并发原语的使用情况。

匿名函数的goroutine使用比普通函数要多,基本每1~5千行代码创建一个goroutine。

虽然Go鼓励消息传递,但是在这些大项目里,共享内存的使用比消息传递要多,Mutex基本在channel的两倍以上。

Bug分类

这篇论文里,按两个维度对bug进行分类:

行为:阻塞和非阻塞,阻塞bug指goroutine意外地阻塞无法继续执行的情况(例如死锁),非阻塞bug通常是数据冲突

原因:共享内存和消息传递,因为用了这两种技术之一导致的bug

可以看到,共享内存其实导致了更多的bug。

阻塞bug

消息传递和共享内存导致的阻塞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

m.Unlock()

}

func goroutine2() {

for{

m.Lock() // 阻塞

m.Unlock()

request

}

}

非阻塞bug

共享内存导致更多的非阻塞bug,几乎是消息传递的8倍。

例如在下面这段代码里,每当ticker触发时执行一次f(),通过stopCh退出循环:

ticker := time.NewTicker()

for {

f()

select {

case

return

case

}

}

但是select是非确定性的,stopCh和ticker同时发生时,不一定会执行stopChan的分支,正确做法是先检查一次stopCh:

ticker := time.NewTicker()

for {

select{

case

return

default:

}

f()

select {

case

return

case

}

}

参考

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP