golang的channel死锁问题

Channel 介绍

默认channel

默认情况下创建的channel是阻塞和不带缓冲区的,例如:

1
ch := make(chan int)  // 创建一个阻塞的不带缓冲区的channel

通过默认方式创建的channel有以下性质:

  • 发送操作将会阻塞,直到接收端准备好了。
  • 接收操作将会阻塞,直到发送端准备好了。也就是说:若channel中没有数据,接收者将会阻塞。

带缓冲区的Channel

不带缓冲区的channel只能包含一个元素(一条记录),带缓冲区的channel可以包含多条记录

1
ch := make(chan string, 100)  // 此时的ch,类似一个消息队列,可以容纳100个string类型的元素
  • 向带缓冲区的channel写数据时不会阻塞,直到channel的缓冲区满了
  • 从带缓冲区的channel中读数据也不会阻塞,直到缓冲区为空
  • 从带缓冲区的channel中读取或写入数据时,是异步的,类比使用消息队列写入和读取数据
  • 向带缓冲区的channel中写数据时是FIFO顺序进行的

Channel 死锁

缺失接收者或者发送者产生的死锁

无缓冲信道在取值前没有发送者或是传之前没有接收者,就会发生死锁

例如

1
2
3
4
func main() {
ch := make(chan string)
ch <- "channelValue"
}

1
2
3
4
func main() {
ch := make(chan string)
<-ch
}

对于第一种例子,可修改为

1
2
3
4
func main() {
ch := make(chan string, 10)
ch <- "channelValue"
}

此时就不会产生死锁,但是这个依然是不推荐的,详情请看内存泄漏

常见因为这种情况产生死锁可归结于以下几种情况:

  • 一个channel在一个主go程里同时进行读和写
1
2
3
4
5
6
7
func main() {
// 死锁1
ch := make(chan int)
ch <- 100
num := <-ch
fmt.Println("num=", num)
}

该程序用的是不带缓存的channel,此时程序运行到ch <- 100因为没有接收者就会阻塞在这里

  • go程开启之前使用channel
1
2
3
4
5
6
7
8
9
10
11
12
func main()  {

ch := make(chan int)
ch <- 100 //此处死锁 优于go程之前使用通道
go func() {
num := <-ch
fmt.Println("num=", num)
}()
//ch <- 100 此处不死锁
time.Sleep(time.Second*3)
fmt.Println("finish")
}

此时程序运行到ch <- 100因为没有接收者就会阻塞在这里

上面程序使用带缓存的channel即可解决死锁问题

1
ch := make(chan int, 10)

读取空Channel或者Channel满了产生的死锁

1
2
3
4
5
6
7
func main() {
// 死锁1
ch := make(chan int)
//close(ch) 向关闭的channel中读取数据 是该数据的类型的零值
num := <-ch
fmt.Println("num=", num)
}

读取空channel会产生死锁

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

超过channel缓存继续写入数据导致死锁

主程和子程相互等待产生的死锁

1
2
3
4
5
6
7
8
9
10
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()

<- ch1
}

上面的代码不能保证是主线程的<-ch1先执行还是子协程的代码先执行。

如果主协程先执行到<-ch1,显然会阻塞等待有其他协程往ch1传值。终于等到子协程运行了,结果子协程运行ch2 <- "ch2 value"就阻塞了,因为是无缓冲,所以必须有下家接收值才行,但是等了半天也没有人来传值。

所以这时候就出现了主协程等子协程的ch1,子协程在等ch2的接收者,ch1<-“ch1 value”语句迟迟拿不到执行权,于是大家都在相互等待,系统看不下去了,判定死锁,程序结束。

即使改成如下:

1
2
3
4
5
6
7
8
9
10
11
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()

<- ch1
<- ch2
}

依然改变不了主程子程相互等待的现状,依然死锁

改成如下:

1
2
3
4
5
6
7
8
9
10
11
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch2 <- "ch2 value"
ch1 <- "ch1 value"
}()

<- ch2
<- ch1
}

即可解决死锁问题

同样有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 死锁3
func main() {
ch1 := make(chan int )
ch2 := make(chan int )

go func() {
for {
select {
case num := <-ch1:
fmt.Println("num=", num)
ch2 <- 100
}
}
}()

for {
select {
case num := <-ch2:
fmt.Println("num=", num)
ch1 <- 300
}
}
}

上面num := <-ch1和 num := <-ch2相互等待导致死锁

不会发生死锁的情况

close解决读取死锁问题

我们可以用range来遍历一个channel,range在访问不到channel里的元素就会一直阻塞住,但是如果channel没有被close,就会发生死锁,如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main(){
taskChan := make(chan int)
// 或 taskChan := make(chan int, 10)

go func() {
for i := 0; i < 10; i++ {
taskChan <- i
}
}()
for task := range taskChan {
fmt.Println(task)
}
}

上面这个情况就会发生死锁

解决的方法是将其close

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main(){
taskChan := make(chan int)
// 或 taskChan := make(chan int, 10)

go func() {
for i := 0; i < 10; i++ {
taskChan <- i
}
close(taskChan)
}()

for task := range taskChan {
fmt.Println(task)
}
}

这样就不会发生死锁

另外,在Channel被close后,向其写数据会导致panic,但是从Channel读取数据直到空之后依然可以继续读数据,此时读出的数据为数据类型的默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main(){
taskChan := make(chan int)
// 或 taskChan := make(chan int, 10)

go func() {
for i := 0; i < 10; i++ {
taskChan <- i
}
close(taskChan)
}()

for task := range taskChan {
fmt.Println(task)
}
value, ok := <-taskChan
fmt.Println(value, ok)
}

上面输出的值是

1
2
3
4
5
6
7
8
9
10
11
0
1
2
3
4
5
6
7
8
9
0 false

子协程的阻塞不会造成死锁

1
2
3
4
5
6
7
8
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
//time.Sleep(time.Second * 3)
//加了也不会死锁
}

上面情况是子协程的ch发生了阻塞,但是主协程已经运行结束后,子协程也只能跟着结束,所以不会发生死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main(){
for i := 0; i < 10; i++ {
go func() {
for task := range taskChan {
fmt.Println(task)
}
}()
}
for i := 0; i < 5; i++ {
taskChan <- i
}

time.Sleep(1e9)
}

上面的协程中都用了range,没有close,但是并不会发生死锁,因为阻塞是发生在协程内的

但是即使不会发生死锁,协程内发生阻塞后的地方就无法继续运行代码,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main(){
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i, "+")
for task := range taskChan {
_ = task
}
fmt.Println(i, "-")
}(i)
}
for i := 0; i < 5; i++ {
taskChan <- i
}

time.Sleep(1e9)
}

输出如下:

1
2
3
4
5
6
7
8
9
10
1 +
0 +
9 +
5 +
2 +
8 +
4 +
3 +
7 +
6 +

但是如果使用close

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main(){
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println(i, "+")
for task := range taskChan {
_ = task
}
fmt.Println(i, "-")
}(i)
}
for i := 0; i < 5; i++ {
taskChan <- i
}

close(taskChan)
time.Sleep(1e9)
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
9 +
9 -
0 +
0 -
6 +
6 -
1 +
1 -
7 +
7 -
3 +
3 -
4 +
4 -
5 +
5 -
8 +
8 -
2 +
2 -

所以使用range也不一定要close,但是推荐用close,避免某些代码无法运行而找不到问题,以及避免内存泄漏

如果是主程序使用range是一定要在其他地方close的。

内存泄漏

1
2
3
4
5
6
func main() {
ch := make(chan string)
go func() {
ch <- "send"
}()
}

对于上面这种情况,虽然不会造成死锁,但是会导致内存泄漏

什么是内存泄露呢,就是是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

比如下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
ch := make(chan string)
var cmd string
for {
fmt.Scanln(&cmd)
if cmd == "c" {
fmt.Println("go")
go func() {
ch <- "send"
}()
cmd = ""
}
}
}

上面程序中的cmd模仿平时业务的调用api,如果调用了该go程序,就会创建一个go程,但是每一个go程都因为ch <- "send"阻塞住了,导致在协程一直无法运行结束,一直占用着内存,堆积起来后就会吃光内存。

参考链接:

https://juejin.cn/post/6844903881843933197

https://blog.csdn.net/Chen_Jeep/article/details/109534566

https://www.cxymm.net/article/zg_hover/80993184