golang channel 使用不当导致的 deadlock
最近开启了19年第二个重要任务,Go Lang
的学习,如果之前只是因为个人爱好的话,现在是因为chaos monkey
很多都是用Go Lang
写的,不得不要抓紧完成这块知识的短板了
今天尝试用goroutine
+ channel
的方式实现一个简单的下载豆瓣图书图片的工作,好像我特别喜欢拿豆瓣来做实验,哈哈,希望豆瓣的同学看到了不要生气
##实现逻辑
代码逻辑其实非常简单,这个要感谢豆瓣的非常 Restful 的 url , 让我可以不用通过 parse HTML 来获取图片链接,直接遍历 id 即可 ,因 为 url 都非常简单 [https://img1.doubanio.com/view/subject/l/public/s10000000.jpg] , 这里再次感谢豆瓣,哈。
主函数
main 的处理逻辑如下
- 创建 N 个 go routine
- 通过 一个 task channel ,来创建不同的下载任务,实际就是自增的方式生成新的下载 url
- 通过一个 result 的channel ,获取 go routine 的任务执行结果
- 等所有任务结束后,通过一个
1 | func main() { |
看似简单的代码,实际上在channel
上👎了很多次坑
坑
all goroutines are asleep - deadlock!
一开始没有用带buffer 的channel ,就发生了all goroutines are asleep - deadlock!
, 感觉很奇怪,google 了半边天发现这个是一个典型的channel
用法错误,尝试做一个非常简单的实验,看起来是不会有任何问题的代码
1 | func main() { |
运行的结果还是老样子
1 | fatal error: all goroutines are asleep - deadlock! |
最后发现[这篇文章][https://my.oschina.net/u/157514/blog/149192]解释的比较清,这里的问题就是处在没有buffer 的channel 上,上面这个例子里,没有buffer 的channel 当你写入的时候,它就会block,直到有另外一个线程拿走这个item,由于我们没有用goroutine,等于说master 的goroutine 就被整个锁住了,也就为什么go runtime 会报 deadlock的问题,
修改成两个goroutine 的模式就没问题了
1 | func main() { |
输出结果
1 | start main2 |
程序又hang住了
上面一个问题解了以后,我把channel改成了这样
1 | // goRoutineSize = 10 |
这里goRoutineSize
表示创建的协程数量,另外还有一个taskNumber
表示任务数量,但是一运行又hang住了,我怀疑还是hang 在了channel 上面,先看一下 worker 的go routine
的逻辑
1 | func spider(task chan ImageTask, result, quit chan int) { |
怀疑是result <- 1
这里block了,尝试分解一下程序的运行
- 先创建10 个
go routine
, task channel 的 buffer 就是 10 - 然后master goroutine 朝 task 里面写入 50 个task,实际是要等 worker 分批拿走任务才能再写入的
- worker 做完任务后,会写入 result ,但是这里 result 的buffer 是 10,所以一旦写满,worker 就会block 在
result <- 1
上,这样他们就不会去获取task
, 然后外部就会block 在task <- …
上,整体就hang住了
修改的方式就很简单了,result 的 buffer size 改成 taskNumber 就好了
1 | quit := make(chan int, goRoutineSize) |
所以说虽然都说go 的并发好写,但是要把channel 理解好,用好,真是任重道远