关于channel发生死锁的情况总结

时刻提醒自己必须深入理解channel的特性,有自己的思考,而不是死记硬背。之后可以阅读熟悉下channel的底层实现原理。

向无缓冲的channel发送/接收数据

无缓冲的channel必须有接收才能发送,下述代码在执行的时候会引发deadlock错误

1
2
3
4
5
func main() {
ch := make(chan int)
ch <- 1 // 这一行代码会引发死锁
// <-ch // 直接取值也会引发死锁
}

解决方法是启用一个goroutine去接收值,如下:

1
2
3
4
5
6
7
8
9
10
func recv(c chan int) {
ret := <-c
fmt.Println(ret)
}

func main() {
ch := make(chan int)
go recv(ch)
ch <- 1
}

思考:下述情况会引发死锁吗?

1
2
3
4
5
6
7
func main() {
ch := make(chan int)
go func() {
ch <- 1
}()
time.Sleep(time.Second * 3)
}

答案是不会引发死锁,虽然子协程一直阻塞在传值语句,但是和主协程之间并无产生联系,当主协程退出的时候子协程也就跟着退出了。
延伸:如果主协程和子协程之间建立了联系会产生死锁吗?

1
2
3
4
5
6
7
8
9
func main() {
ch1 := make(chan, int)
ch2 := make(chan, int)
go func() {
ch2 <- 21
ch1 <- 11
}()
<-ch1
}

输出有缓冲的channel中所有的值

当读取完channel中的数据后,继续读取的操作会造成阻塞,且阻塞发生在主协程中,故会引发阻塞。

1
2
3
4
5
6
7
8
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
for ch := range ch {
fmt.Println(ch)
}
}

解决方法是发送完所有数据后则关闭channel,或者通过select方法中的default进行处理,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
// solution 1:
// close(ch)

// solution 2:
// select {
// case v := <-ch:
// fmt.Println(v)
// default:
// fmt.Println("nothing in channel")
// }
for ch := range ch {
fmt.Println(ch)
}
}

过度向有缓冲的channel写入数据

写入数据超过channel的容量的时候,也会引发死锁。

1
2
3
4
5
6
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3
}

解决方法是通过select方法中的default进行处理:

1
2
3
4
5
6
7
8
9
10
11
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
select {
case ch <- 3:
fmt.Println("ok")
default:
fmt.Println("wrong")
}
}

总结

上述提到的死锁,是指在程序的主协程中发生的情况,如果上述情况是发生在非主协程中,读取或者写入的情况是发生阻塞的,而不是死锁(此时需要考虑是否需要主动关闭子协程)。实际上,阻塞情况省去了我们加锁的步骤,反而是更加有利于代码编写,要合理的运用阻塞。