理解真实世界中 Go 的并发 BUG( 二 )


理解真实世界中 Go 的并发 BUG文章插图
img
Bug原因分析1、阻塞性bug统计如下:
理解真实世界中 Go 的并发 BUG文章插图
img
具体分析
(1)对共享内存保护的失误:
Mutex:28个阻塞性bug由对锁的不当使用造成 , 包括重复锁、以冲突的顺序申请锁、忘记解锁* 。 这些bug都是传统bug , 文章觉得传统的死锁检测算法应该能检测出这类bug 。
RWMutex:前面提到过 , go中的写锁优先级高 。 这种实现机制可以造成如下bug:协程A对同一个RWMutex申请两次读锁 , 但在这两次申请中间 , 协程B申请写锁 。 此时 , 由于A已经持有了一个读锁 , 而写锁又是排他性的 , 所以B被阻塞 。 然后 , A第二次申请读锁时 , 由于B的写锁优先级高 , 所以A的读锁必须排在B的写锁请求之后 , 导致A被阻塞 。 从而发生了死锁 。
统计中有5个bug是由这个原因造成 。 由于在C语言中这种情况不会造成死锁 , 所以参考C语言类似机制在Go中写这样的代码 , 容易导致这样的bug 。
Wait:3个阻塞性bug归因于等待操作无法继续 。 跟Mutex和RWMutex不同 , 这里并不涉及循环等待 。 有两个bug是这样的:Cond被用来保护共享内存访问 , 其中一个协程调用了Cond.Wait() , 但是在这之后却没有别的协程调用Cond.Signal()(或Cond.Broadcast()) 。
另一个bug , Docker#25384 , 如下图所示 , 使用了一个共享的WaitGroup变量 , 造成bug主要是Wait()放在了错误的地方即第7行 , 修复bug只需要把Wait()挪到图中的第8行(循环外) 。
理解真实世界中 Go 的并发 BUG文章插图
img
(2)对消息传递的误用
【理解真实世界中 Go 的并发 BUG】Channel:对通过channel传递消息的错误使用导致了29个阻塞性bug 。 很多都跟发送和接收的错配有关 。 如下图所示 , 在使用第2行代码初始化channel的情况下 , 在子协程执行到第6行代码前 , 如果超时时间到了 , 或者子协程执行到第6行时 , select的两个case同时可用 , 由于select的随机性而跑到了超时的那个case , 就会导致finishReq函数返回 , 从而子协程阻塞 。 这个问题的修复方法是将channel定义为缓冲channel , 这样无论何种情况子协程都不会阻塞住 。
理解真实世界中 Go 的并发 BUG文章插图
img
当组合使用go特定类库时 , channel的创建和协程阻塞有可能被埋在了类库的调用之中 。 如下图所示 , 行1创建了一个新的context对象 hcancel , 同时一个新的协程被创建 , 消息可以通过hcancel的channel传递到新协程 。 如果在行4 timeout大于0 , 另一个context对象在行5被创建 , 并且hcancel指向了新的对象 。 之后 , 将无法向协程所关联的旧对象发送消息 , 旧对象也没法被关闭 。 这个问题的避免方法是 , 避免创建额外的context对象 。
理解真实世界中 Go 的并发 BUG文章插图
img
Channel和其他的阻塞特性:有16个bugs , 其中一个协程阻塞在Channel操作 , 而别的协程阻塞在锁或等待上 。 如下图 , 协程1在发送消息到ch时阻塞了 , 而同时协程2却被m.Lock()阻塞 。 解决方案是对协程1使用具有default分支的select来确保ch不再阻塞 。
理解真实世界中 Go 的并发 BUG文章插图
img
消息库函数:go提供了几种传递消息和数据的库 , 如Pipe 。 对这些的不正确使用也会造成bug 。 例如 , 和Channel类似 , 如果一个Pipe未关闭 , Pipe的两端一个伙伴挂了 , 另一个伙伴等着读或写数据 , 那这是等着读或写数据的伙伴就被阻塞住了 。 类似的bug有4个 。