Go语言sync.WaitGroup用法上可能会出现的坑
Go自带的包sync中有一个工具WaitGroup,从名字就可以看出,它可以帮助我们控制并发的流程。
Go的并发是通过goroutine来处理,有时候我们希望控制某一组routine的执行,比如全部执行完了,再进行下一步,这时会想到goroutine的搭档channel,我们可以通过channel的阻塞,来完成流程控制。
不过有了sync.WaitGroup,代码写起来会清晰许多,它的用法非常简单,只有三个方法:
- Add(delta int)
- Done()
- Wait()
通过Add,增加需要等待的任务数,Done会将数目减一,而Wait会将程序阻塞,知道Done方法将数目降至为0。
这是一个具体的例子:
1 | func main() { |
上面的例子中,for循环启用了10条goroutine,而在主程序中,使用wg.Wait()等待协程的完成。
好了,上面只是铺垫,之所以记录这篇文,主要还是因为看到有文章在介绍WaitGroup时,有隐性的bug存在。
比如说,把上面的代码改成这样:
1 | func main() { |
这里就会出现问题了,可能n的数值比较小,运行很多次都未必出现问题,counter绝大概率下还是会输出10。
但这正是并发程序的可怕之处,运行几个月才出现问题,往往让人一头雾水。
把n调到100、1000,问题就出现了,我们会发现,wg.Wait()失灵了,它没等到所有goroutine执行完毕,就已经接着走了。
问题就在于wg.Add(1),我们知道wg.Wait()只要判断它自身没有在等待的任务,便不锁住程序,那只要goroutine的执行够快,下一条wg.Add(1)还来不及运行的时候,就被wg.Done()抢先一步,自然wg.Wait()就完成了。
不知道上面的描述你能否理解,其实类似这种问题,可能并非所有人都能一时明白过来,更多的是靠理解与经验,所以并发容易出问题也是这个原因。
关于WaitGroup我还看到另外一个记录的问题,就是有些童鞋将它传递到具体的函数里面去执行,然后就发现它不生效了,这个跟Go的引用与拷贝有关,如果直接传入WaitGroup,实际上它不是一个引用,而是一份拷贝,那么如果是在里面进行的Add操作,那必然对原先的参数是不起作用的,这一点需要去看更多关于Go函数传值的内容。
- 本文链接:https://keepmoving.ren/golang/waitgroup/
- 版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!
