1 为什么会有信道

  协程(goroutine)算是Go的一大新特性,也正是这个大杀器让Go为很多路人驻足欣赏,让信徒们为之欢呼津津乐道。

  协程的使用也很简单,在Go中使用关键字“go“后面跟上要执行的函数即表示新启动一个协程中执行功能代码。

  1. func main() {
  2. go test()
  3. fmt.Println("it is the main goroutine")
  4. time.Sleep(time.Second * 1)
  5. }
  6.  
  7. func test() {
  8. fmt.Println("it is a new goroutine")
  9. }

 

  可以简单理解为,Go中的协程就是一种更轻、支持更高并发的并发机制。

  仔细看上面的main函数中有一个休眠一秒的操作,如果去掉该行,则打印结果中就没有“it is a new goroutine”。这是因为新启的协程还没来得及运行,主协程就结束了。

  所以这里有个问题,我们怎么样才能让各个协程之间能够知道彼此是否执行完毕呢?

  显然,我们可以通过上面的方式,让主协程休眠一秒钟,等等子协程,确保子协程能够执行完。但作为一个新型语言不应该使用这么low的方式啊。连Java这位老前辈都有Future这种异步机制,而且可以通过get方法来阻塞等待任务的执行,确保可以第一时间知晓异步进程的执行状态。

  所以,Go必须要有过人之处,即另一个让路人侧目,让信徒为之疯狂的特性——信道(channel)。

2 信道如何使用

  信道可以简单认为是协程goroutine之间一个通信的桥梁,可以在不同的协程里互通有无穿梭自如,且是线程安全的。

2.1 信道分类

  信道分为两类

无缓冲信道

  1. ch := make(chan string)

  

有缓冲信道

  1. ch := make(chan string, 2)

  

2.2 两类信道的区别

  1、从声明方式来看,有缓冲带了容量,即后面的数字,这里的2表示信道可以存放两个stirng类型的变量

  2、无缓冲信道本身不存储信息,它只负责转手,有人传给它,它就必须要传给别人,如果只有进或者只有出的操作,都会造成阻塞。有缓冲的可以存储指定容量个变量,但是超过这个容量再取值也会阻塞。

2.3 两种信道使用举例

无缓冲信道

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

  

  在主协程中新启一个协程且是匿名函数,在子协程中向通道发送“send”,通过打印结果,我们知道在主线程使用<-ch接收到了传给ch的值。

  <-ch是一种简写方式,也可以使用str := <-ch方式接收信道值。

  上面是在子协程中向信道传值,并在主协程取值,也可以反过来,同样可以正常打印信道的值。

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

  

有缓冲信道

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

  

  执行结果为

  1. first
  2. second

  

  信道本身结构是一个先进先出的队列,所以这里输出的顺序如结果所示。

  从代码来看这里也不需要重新启动一个goroutine,也不会发生死锁(后面会讲原因)。

3 信道的关闭和遍历

3.1 关闭

  信道是可以关闭的。对于无缓冲和有缓冲信道关闭的语法都是一样的。

  1. close(channelName)

  注意信道关闭了,就不能往信道传值了,否则会报错。

  1. func main() {
  2. ch := make(chan string, 2)
  3. ch <- "first"
  4. ch <- "second"
  5.  
  6. close(ch)
  7.  
  8. ch <- "third"
  9. }

  报错信息

  1. panic: send on closed channel

  

3.2 遍历

  有缓冲信道是有容量的,所以是可以遍历的,并且支持使用我们熟悉的range遍历。

  1. func main() {
  2. chs := make(chan string, 2)
  3. chs <- "first"
  4. chs <- "second"
  5.  
  6. for ch := range chs {
  7. fmt.Println(ch)
  8. }
  9. }

  

  输出结果为

  1. first
  2. second
  3. fatal error: all goroutines are asleep - deadlock!

  没错,如果取完了信道存储的信息再去取信息,也会死锁(后面会讲)

4 信道死锁

  有了前面的介绍,我们大概知道了信道是什么,如何使用信道。

  下面就来说说信道死锁的场景和为什么会死锁(有些是自己的理解,可能有偏差,如有问题请指正)。

4.1 死锁现场1

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

   这两种情况,即无论是向无缓冲信道传值还是取值,都会发生死锁。

原因分析

  如上场景是在只有一个goroutine即主goroutine的,且使用的是无缓冲信道的情况下。

  前面提过,无缓冲信道不存储值,无论是传值还是取值都会阻塞。这里只有一个主协程的情况下,第一段代码是阻塞在传值,第二段代码是阻塞在取值。因为一直卡住主协程,系统一直在等待,所以系统判断为死锁,最终报deadlock错误并结束程序。

延伸

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

  这种情况不会发生死锁。

  有人说那是因为主协程发车太快,子协程还没看到,车就开走了,所以没来得及抱怨(deadlock)就结束了。

  其实不是这样的,下面举个反例

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

  这次主协程等你了三秒,三秒你总该完事了吧?!

    但是从执行结果来看,并没有子协程因为一直阻塞就造成报死锁错误。

  这是因为虽然子协程一直阻塞在传值语句,但这也只是子协程的事。外面的主协程还是该干嘛干嘛,等你三秒之后就发车走人了。因为主协程都结束了,所以子协程也只好结束(毕竟没搭上车只能回家了,光杵在哪也于事无补)

4.2 死锁现场2

  紧接着上面死锁现场1的延伸场景,我们提到延伸场景没有死锁是因为主协程发车走了,所以子协程也只能回家。也就是两者没有耦合的关系。

  如果两者通过信道建立了联系还会死锁吗?

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

  

  执行结果为

  1. fatal error: all goroutines are asleep - deadlock!

  没错,这样就会发生死锁。

原因分析

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

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

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

  相反执行顺序也是一样。

延伸

  有人会说那我改成这样能避免死锁吗

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

  不行,执行结果依然是死锁。因为这样的顺序还是改变不了主协程和子协程相互等待的情况,即死锁的触发条件。

  改为下面这样就可以正常结束

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

  

  借此,通过下面的例子再验证上面死锁现场1是因为主协程没受到死锁的影响所以不会报死锁错误的问题

  1. func main() {
  2. ch1 := make(chan string)
  3. ch2 := make(chan string)
  4. go func() {
  5. ch2 <- "ch2 value"
  6. ch1 <- "ch1 value"
  7. }()
  8.  
  9. go func() {
  10. <- ch1
  11. <- ch2
  12. }()
  13.  
  14. time.Sleep(time.Second * 2)
  15. }

  

  我们刚刚看到如果

  1. <- ch1
  2. <- ch2

  放到主协程,则会因为相互等待发生死锁。但是这个例子里,将同样的代码放到一个新启的协程中,尽管两个子协程存在阻塞死锁的情况,但是不会影响主协程,所以程序执行不会报死锁错误。

4.3 死锁现场3

  1. func main() {
  2. chs := make(chan string, 2)
  3. chs <- "first"
  4. chs <- "second"
  5.  
  6. for ch := range chs {
  7. fmt.Println(ch)
  8. }
  9. }

  

  输出结果为

  1. first
  2. second
  3. fatal error: all goroutines are asleep - deadlock!

  

原因分析

  为什么会在输出完chs信道所有缓存值后会死锁呢?

  其实也很简单,虽然这里的chs是带有缓冲的信道,但是容量只有两个,当两个输出完之后,可以简单的将此时的信道等价于无缓冲的信道。

  显然对于无缓冲的信道只是单纯的读取元素是会造成阻塞的,而且是在主协程,所以和死锁现场1等价,故而会死锁。

5 总结

1、信道是协程之间沟通的桥梁

2、信道分为无缓冲信道和有缓冲信道

3、信道使用时要注意是否构成死锁以及各种死锁产生的原因

如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。

Go语言学习——channel的死锁其实没那么复杂的更多相关文章

  1. go语言学习--channel的关闭

    在使用Go channel的时候,一个适用的原则是不要从接收端关闭channel,也不要在多个并发发送端中关闭channel.换句话说,如果sender(发送者)只是唯一的sender或者是chann ...

  2. 深度解密Go语言之channel

    目录 并发模型 并发与并行 什么是 CSP 什么是 channel channel 实现 CSP 为什么要 channel channel 实现原理 数据结构 创建 接收 发送 关闭 channel ...

  3. GO语言之channel

    前言: 初识go语言不到半年,我是一次偶然的机会认识了golang这门语言,看到他简洁的语法风格和强大的语言特性,瞬间有了学习他的兴趣.我是很看好go这样的语言的,一方面因为他有谷歌主推,另一方面他确 ...

  4. 技能收获与C语言学习

    你有什么技能比大多人(超过90%以上)更好? 我会的东西很多,喜欢的东西太多,但是很遗憾广而不专,会而不精.学了很多东西我都是为了娱乐,因为以前我们那里过于强调学习,很多爱好也都被扼杀在摇篮里.我觉得 ...

  5. 20155206赵飞技能获取经验,C语言学习感想与对JAVA的学习目标

    自己较强的技能获取经验. 1:实话实说我自己是没有哪个技能可以超过90%的人的,只有自认为做的还可以的一些事情,例如打篮球,office软件的应用,一百米跑.至于其他方面就是很平庸了. 2:经验主要有 ...

  6. 20155229-付钰涵-分析自我技能延展到c语言学习状况

    我的小技能 我记得幼儿园时表演的舞蹈,也记得从水彩到素描的学习,还记得小学和初中获得的钢琴省级奖项. 舞蹈止于一年级,绘画止于三年级,钢琴从学前班到高一那十年的时间里有过断续. 03年-04年的那个冬 ...

  7. linux 下C语言学习路线

    UNIX/Linux下C语言的学习路线.一.工具篇“公欲善其事,必先利其器”.编程是一门实践性很强的工作,在你以后的学习或工作中,你将常常会与以下工具打交道, 下面列出学习C语言编程常常用到的软件和工 ...

  8. 20155224聂小益 - 我的技能与C语言学习

    20155224聂小益 - 我的技能与C语言学习 预备作业2 ● 你有什么技能比大多人(超过90%以上)更好? 我认为我可能有些技能身边有的人比较少有,但是要是超过90%以上我实在不敢保证.毕竟厉害的 ...

  9. 20155226田皓宇关于优秀技能经验以及c语言学习感悟和对JAVA的展望

    读老师文章后关于一项优秀技能的经验有感 1.首先我自我剖析认为,我是没有哪一个方面能做到强于身边90%的人的,我只能说有些方面略强于身边的人.比如唱歌.办公软件的应用(word.excel)等.但我不 ...

随机推荐

  1. crossplatform---Nodejs in Visual Studio Code 02.学习Nodejs

    1.开始 源码下载:https://github.com/sayar/NodeMVA 在线视频:https://mva.microsoft.com/en-US/training-courses/usi ...

  2. FastDFS是纯C语言实现,只支持Linux,适合以中小文件为载体的在线服务,还可以冗余备份和负载均衡

    一.理论基础 FastDFS比较适合以中小文件为载体的在线服务,比如跟NGINX(APACHE)配合搭建图片服务器. 分布式文件系统FastDFS FastDFS是纯C语言实现,只支持Linux.Fr ...

  3. sql server中使用链接服务器访问oracle数据库

    一.  安装配置oracle客户端 要访问orcale数据,必须在访问的客户端机器上安装oracle客户端. Orcale有两种形式的客户端: l         完整的客户端 包括访问服务器端数据库 ...

  4. LeapMotion Demo2

    原文:LeapMotion Demo2    官方doc有四个手势,最近尝试实现对握拳的识别,并能在我的程序界面上体现出来.    调试过程较为繁琐,幸好最终效果还差强人意! 首先看看我的效果图:   ...

  5. IIS IP地址与端口

    IP地址 全部未分配,则以下所有IP对应端口都可以访问网站指定IP,则只有指定IP可以访问网站   1 端口 可以在建立网站之后继续添加端口,则所有添加的端口均可以访问   2   3

  6. delphi判断线程状态函数(使用GetExitCodeThread API函数去判断线程的句柄)

    //判断线程是否释放//返回值:0-已释放:1-正在运行:2-已终止但未释放://3-未建立或不存在 function CheckThreadFreed(aThread: TThread): Byte ...

  7. JavaScript严格模式分析

    简要:严格模式(strict mode)是JavaScript在ES5里面新增的编码模式,只要一行代码 就可开启,可谓 非常简单了,而它对于 我们的编码来说到底有什么不同呢? 一. 严格模式的目的? ...

  8. IDisposeable 最佳实现

    public class MyClass : IDisposable { #region 变量声明 // 指向外部非托管资源 private IntPtr handle; // 此类使用的其它托管资源 ...

  9. 为DataGridTemplateColumn设置快捷菜单

    <DataGrid.ContextMenu> <ContextMenu> <MenuItem Command="{x:Static ApplicationCom ...

  10. uwp汉堡菜单的实现

    ---恢复内容开始--- 现在uwp上面的汉堡菜单(就是那个三道杠,点击之后会出现菜单)使用的越来越普遍,比如微软自己家的Cortana.现在我使用的实现方法是使用SplitView实现.首先Spli ...