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


img
WaitGroup的误用:使用WaitGroup的一个基本准则是 , Add必须在Wait之前执行 。 有6个bug是因为违反了这条准则 。 如下图所示 , 这是etcd中的一个bug , 这里是无法保证func1中行8的Add一定在func2中行5的Wait之前执行的 。 解决方案就是将Add操作遇到行6的位置 , 保证要么Add在Wait之前执行 , 要么根本不会执行到idle这个case 。
理解真实世界中 Go 的并发 BUG文章插图
img
特定库函数:go中有些类库的变量是隐式在多协程中共享的 。 如context就被设计为可以被多个关联协程访问 。 etcd#7816就是因为在多个协程中竞争使用一个context对象的一个字符串字段导致的 。
另一个例子是testing包 。 测试函数只有一个testing.T类型的变量 , 这个变量用于传递测试状态如error何日志 。 有3个bug就是在测试函数以及测试函数内启动的子协程之间竞争使用testing.T变量导致 。
(2)消息传递中的错误
channel的误用:前面也提到过 , channel的使用需要遵循一定的规则 , 否则就会引起一些bug 。 如下图所示(Docker#24007) , 可能有多个协程会运行到这段代码 , 其中可能有多个跑到了select的default分支 , 导致对channel的多次关闭 , 从而引发panic 。 这种情况 , 可以使用Once.Do将关闭channel的语句包起来 , 保证它只会执行一次 。
理解真实世界中 Go 的并发 BUG文章插图
img
还有一种类型是将channel和select一起使用 , 当select收到多个case的消息时 , 是没办法保证会执行哪一个的 , 这种非确定性的选择 , 导致了3个bug 。 下图是一个例子 , 其中f函数执行耗时操作 , 当它执行完之后 , stopCh的消息和ticker有可能同时到达 , 此时并不一定会执行到11行return语句 , 也有可能执行到case <- ticker 这里 , 从而继续循环 , f()没必要地多执行了一次 。 这种情况下 , 应该在f()执行的前后都判断一下是否该退出循环 。
理解真实世界中 Go 的并发 BUG文章插图
img
特定库函数:一些库函数内部会使用channel , 也可能导致非阻塞性bug 。 下图是一个与time包有关的bug 。 开发者想实现的是 , 要么收到Done信号 , 要么超时 , 然后再返回 。 但是含bug的版本先创建了超时时间为0的timer , 然后再判断参数dur是否大于0, 大于0的话修改timer 。 但是 , 当dur为0的情况下 , timer实际上一开始就被设置为有信号了 , 可能导致函数过早返回 。 解决方案是不要让timer过早创建 。
理解真实世界中 Go 的并发 BUG文章插图
img
非阻塞性bug的检测Go提供了数据竞争检测 , 在build的时候使用 -race 标志即可启用 。
文章的一些结论是 , 消息传递机制也容易造成bug , 情况并不比共享内存机制好 。 消息传递机制更多地会造成一些阻塞性bug , 比较少造成非阻塞性bug , 而且可以用于解决由于共享内存导致的非阻塞性bug 。
关于bug检测 , 目前很多在传统语言中针对共享内存的检测算法 , 在go中也是适用的 , 但是针对go的消息传递机制所引起bug的检测 , 还需研究 。
译者:Darlzan
【理解真实世界中 Go 的并发 BUG】译文链接: