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 的处理逻辑如下

  1. 创建 N 个 go routine
  2. 通过 一个 task channel ,来创建不同的下载任务,实际就是自增的方式生成新的下载 url
  3. 通过一个 result 的channel ,获取 go routine 的任务执行结果
  4. 等所有任务结束后,通过一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func main() {
//fmt.Println("start crawl the douban site")
init_directory()

// init the main process variables
quit := make(chan int, goRoutineSize)
task := make(chan ImageTask, goRoutineSize)
result := make(chan int, taskNumber)

// init the goroutine
for i := 0; i < goRoutineSize; i++ {
spider(task, result, quit)
}

// init the task
for i := 0; i < taskNumber; i++ {
taskId := taskOffsize + i
url := fmt.Sprintf("https://img1.doubanio.com/view/subject/l/public/s%d.jpg", taskId)
task <- ImageTask{url: url, tid: taskId}
}

// wait all the goroutine done
for i := 0; i < taskNumber; i++ {
<-result
}

for i := 0; i < goRoutineSize; i++ {
quit <- 1
}

close(quit)
close(result)
close(task)
fmt.Println("finish the task")
}

看似简单的代码,实际上在channel上👎了很多次坑

all goroutines are asleep - deadlock!

一开始没有用带buffer 的channel ,就发生了all goroutines are asleep - deadlock!, 感觉很奇怪,google 了半边天发现这个是一个典型的channel用法错误,尝试做一个非常简单的实验,看起来是不会有任何问题的代码

1
2
3
4
5
6
func main() {
fmt.Println("start main2")
ch := make(chan int)
ch <- 1
fmt.Println(<-ch)
}

运行的结果还是老样子

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
/Users/ylshao/code/lab/douban_spider/src/github.com/markshao/main2.go:9 +0xbc

最后发现[这篇文章][https://my.oschina.net/u/157514/blog/149192]解释的比较清,这里的问题就是处在没有buffer 的channel 上,上面这个例子里,没有buffer 的channel 当你写入的时候,它就会block,直到有另外一个线程拿走这个item,由于我们没有用goroutine,等于说master 的goroutine 就被整个锁住了,也就为什么go runtime 会报 deadlock的问题,

修改成两个goroutine 的模式就没问题了

1
2
3
4
5
6
7
8
9
func main() {
fmt.Println("start main2")
ch := make(chan int)
go func() {
ch <- 1
}()

fmt.Println(<-ch)
}

输出结果

1
2
start main2
1

程序又hang住了

上面一个问题解了以后,我把channel改成了这样

1
2
3
4
5
// goRoutineSize = 10
// taskNumber = 50
quit := make(chan int, goRoutineSize)
task := make(chan ImageTask, goRoutineSize)
result := make(chan int, goRoutineSize)

这里goRoutineSize表示创建的协程数量,另外还有一个taskNumber表示任务数量,但是一运行又hang住了,我怀疑还是hang 在了channel 上面,先看一下 worker 的go routine的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func spider(task chan ImageTask, result, quit chan int) {
go func() {
for {
select {
case t := <-task:
downloadImage(t)
result <- 1
fmt.Printf("finish downloa the task %v \n", t)
case <-quit:
fmt.Println("finish the go routine")
return
default:
time.Sleep(50 * time.Microsecond)
}
}
}()
}

怀疑是result <- 1 这里block了,尝试分解一下程序的运行

  1. 先创建10 个 go routine, task channel 的 buffer 就是 10
  2. 然后master goroutine 朝 task 里面写入 50 个task,实际是要等 worker 分批拿走任务才能再写入的
  3. worker 做完任务后,会写入 result ,但是这里 result 的buffer 是 10,所以一旦写满,worker 就会block 在 result <- 1 上,这样他们就不会去获取task , 然后外部就会block 在 task <- …上,整体就hang住了

修改的方式就很简单了,result 的 buffer size 改成 taskNumber 就好了

1
2
3
quit := make(chan int, goRoutineSize)
task := make(chan ImageTask, goRoutineSize)
result := make(chan int, taskNumber)

所以说虽然都说go 的并发好写,但是要把channel 理解好,用好,真是任重道远

[https://my.oschina.net/u/157514/blog/149192)]: