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 | func main() { |
或
1 | func main() { |
对于第一种例子,可修改为
1 | func main() { |
此时就不会产生死锁,但是这个依然是不推荐的,详情请看内存泄漏
常见因为这种情况产生死锁可归结于以下几种情况:
- 一个channel在一个主go程里同时进行读和写
1 | func main() { |
该程序用的是不带缓存的channel,此时程序运行到ch <- 100因为没有接收者就会阻塞在这里
- go程开启之前使用channel
1 | func main() { |
此时程序运行到ch <- 100因为没有接收者就会阻塞在这里
上面程序使用带缓存的channel即可解决死锁问题
1 | ch := make(chan int, 10) |
读取空Channel或者Channel满了产生的死锁
1 | func main() { |
读取空channel会产生死锁
1 | func main() { |
超过channel缓存继续写入数据导致死锁
主程和子程相互等待产生的死锁
1 | func main() { |
上面的代码不能保证是主线程的<-ch1先执行还是子协程的代码先执行。
如果主协程先执行到<-ch1,显然会阻塞等待有其他协程往ch1传值。终于等到子协程运行了,结果子协程运行ch2 <- "ch2 value"就阻塞了,因为是无缓冲,所以必须有下家接收值才行,但是等了半天也没有人来传值。
所以这时候就出现了主协程等子协程的ch1,子协程在等ch2的接收者,ch1<-“ch1 value”语句迟迟拿不到执行权,于是大家都在相互等待,系统看不下去了,判定死锁,程序结束。
即使改成如下:
1 | func main() { |
依然改变不了主程子程相互等待的现状,依然死锁
改成如下:
1 | func main() { |
即可解决死锁问题
同样有
1 | // 死锁3 |
上面num := <-ch1和 num := <-ch2相互等待导致死锁
不会发生死锁的情况
close解决读取死锁问题
我们可以用range来遍历一个channel,range在访问不到channel里的元素就会一直阻塞住,但是如果channel没有被close,就会发生死锁,如下例子:
1 | func main(){ |
上面这个情况就会发生死锁
解决的方法是将其close
1 | func main(){ |
这样就不会发生死锁
另外,在Channel被close后,向其写数据会导致panic,但是从Channel读取数据直到空之后依然可以继续读数据,此时读出的数据为数据类型的默认值。
1 | func main(){ |
上面输出的值是
1 | 0 |
子协程的阻塞不会造成死锁
1 | func main() { |
上面情况是子协程的ch发生了阻塞,但是主协程已经运行结束后,子协程也只能跟着结束,所以不会发生死锁
1 | func main(){ |
上面的协程中都用了range,没有close,但是并不会发生死锁,因为阻塞是发生在协程内的
但是即使不会发生死锁,协程内发生阻塞后的地方就无法继续运行代码,如
1 | func main(){ |
输出如下:
1 | 1 + |
但是如果使用close
1 | func main(){ |
输出如下:
1 | 9 + |
所以使用range也不一定要close,但是推荐用close,避免某些代码无法运行而找不到问题,以及避免内存泄漏。
如果是主程序使用range是一定要在其他地方close的。
内存泄漏
1 | func main() { |
对于上面这种情况,虽然不会造成死锁,但是会导致内存泄漏
什么是内存泄露呢,就是是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
比如下面这个例子
1 | func main() { |
上面程序中的cmd模仿平时业务的调用api,如果调用了该go程序,就会创建一个go程,但是每一个go程都因为ch <- "send"
阻塞住了,导致在协程一直无法运行结束,一直占用着内存,堆积起来后就会吃光内存。
参考链接:
https://juejin.cn/post/6844903881843933197