Golang, 以17个简短代码片段,切底弄懂 channel 基础

(原创出处:https://cloud.tencent.com/developer/user/1148436/activities)

前序:

  因为打算自己搞个基于Golang的IM服务器,所以复习了下之前一直没怎么使用的协程、管道等高并发编程知识。发现自己的channel这块,也就是管道,实在是有些混乱。然后对着文档,边参照官网例子和在编译器测试,总结了下面这17个例子,设置为简短的片段,是为了免得混淆太多,阻碍理解。内含注释丰富,复制粘贴就能编译使用。

  这里立个 flag,有错误欢迎指出,只要你跟着敲完这17个例子,channel的基础绝对可以掌握!

基本概念:

  关于管道 Channel:

    Channels用来同步并发执行的函数并提供它们某种传值交流的机制。

    Channels的一些特性:通过channel传递的元素类型、容器(或缓冲区)和传递的方向由“<-”操作符指定。

    c<-123,把值123输入到管道 c,<-c,把管道 c 的值读取到左边,value :=<-c,这样就是读到 value里面。

  管道分类:

无缓冲的与有缓冲channel有着重大差别,那就是一个是同步的 一个是非同步的。

   比如

   c1:=make(chan int)         无缓冲

   c2:=make(chan int,1)      有缓冲

   例如:c1<-1      

      无缓冲: 不仅仅是向 c1 通道放 1,而是一直要等有别的携程 <-c1 接手了这个参数,那么c1<-1才会继续下去,要不然就一直阻塞着。

      有缓冲: c2<-1 则不会阻塞,因为缓冲大小是1(其实是缓冲大小为0),只有当放第二个值的时候,第一个还没被人拿走,这时候才会阻塞。

例子s

演示 无缓存 和 有缓冲 的 channel 的样子

1 func test0(){
2     /** 演示 无缓存 和 有缓冲 的 channel 的样子 */
3     done  := make(chan bool)   /** 无缓冲 */
4     done1 := make(chan bool,1) /** 有缓冲 */
5     println(done,done1)
6 }

演示 无缓冲在同一个main里面的 死锁例子

1 func test1()  {
2     /** 编译错误 deadlock,阻死 main 进程 */
3     /** 演示 无缓冲在同一个main里面的 死锁例子 */
4     done := make(chan bool)
5     done<-true      /** 这句是输入值,它会一直阻塞,等待读取 */
6     <-done          /** 这句是读取,但是在上面已经阻死了,永远走不到这里 */
7     println("完成")
8 }
演示仅有 输入 语句,但没 读取语句 的死锁例子
1 func test2()  {
2     /** 编译错误 deadlock,阻死 main 进程 */
3     /** 演示仅有 输入 语句,但没 读取语句 的死锁例子 */
4     done := make(chan bool)
5     done<-true  /** 输入,一直等待读取,哪怕没读取语句 */
6     println("完成")
7 }
演示仅有 读取 语句,但没 输入语句 的死锁例子
1 func test3()  {
2     /** 编译错误 deadlock,阻死 main 进程 */
3     /** 演示仅有 读取 语句,但没 输入语句 的死锁例子 */
4     done := make(chan bool)
5     <-done    /** 读取输出,前面没有输入语句,done 是 empty 的,所以一直等待输入 */
6 
7     println("完成")
8 }
演示,协程的阻死,不会影响 main
 1 func test4()  {
 2     /** 编译通过 */
 3     /** 演示,协程的阻死,不会影响 main */
 4     done := make(chan bool)
 5     go func() {
 6         <-done /** 一直等待 */
 7     }()
 8     println("完成")
 9     /**
10      * 控制台输出:
11      *       完成
12      */
13 }
在 test4 的基础上,无缓冲channel在协程 go routine 里面阻塞死
 1 func test5()  {
 2     /** 编译通过 */
 3     /** 在 test4 的基础上,无缓冲channel在协程 go routine 里面阻塞死 */
 4     done := make(chan bool)
 5     go func() {
 6         println("我可能会输出哦") /** 阻塞前的语句 */
 7         done<-true  /** 这里阻塞死,但是上面那句有可能输出,见 test3 的结论 */
 8         println("我永远不会输出")
 9         <-done      /** 这句也不会走到,除非在别的协程里面读取,或者在 main */
10     }()
11     println("完成")
12 }
编译通过,在 test5 的基础上演示,延时 main 的跑完
 1 func test6()  {
 2     /** 编译通过,在 test5 的基础上演示,延时 main 的跑完 */
 3     done := make(chan bool)
 4     go func() {
 5         println("我可能会输出哦")
 6         done<-true  /** 这里阻塞死 */
 7         println("我永远不会输出")
 8         <-done      /** 这句也不会走到 */
 9     }()
10     time.Sleep(time.Second * 1)  /** 加入延时 1 秒 */
11     println("完成")
12     /**
13      * 控制台输出:
14      *       我可能会输出哦
15      *       完成
16      */
17     /**
18      * 结论:
19      *    如果在 go routine 中阻塞死,也可能不会把阻塞语句前的内容输出,
20      *    因为main已经跑完了,所以延时一会,等待 go routine
21      */
22 }
演示无缓冲channel 在 不同的位置里面接收填充和接收
 1 func test7()  {
 2     /** 编译通过,演示无缓冲channel 在 不同的位置里面接收填充和接收*/
 3     done := make(chan bool)
 4     go func() {
 5         done<-true  /** 直到,<-done 执行,否则这里阻塞死 */
 6         println("我永远不会输出,除非 <-done 执行")
 7 
 8     }()
 9     <-done      /** 这里接收,在输出完成之前,那么上面的语句将会走通 */
10     println("完成")
11     /**
12      * 控制台输出:
13      *       我永远不会输出,除非 <-done 执行
14      *       完成
15      */
16 }
演示无缓冲channel 在不同地方接收的影响
 1 func test8()  {
 2     /** 编译通过,演示无缓冲channel 在不同地方接收的影响 */
 3     done := make(chan bool)
 4     go func() {
 5         done<-true  /** 直到,<-done 执行,否则这里阻塞死 */
 6         println("我永远不会输出,除非 <-done 执行")
 7     }()
 8     println("完成")
 9     <-done      /** 这里接收,在输出完成之后 */
10     /**
11      * 控制台输出:
12      *       完成
13      *       我永远不会输出,除非 <-done 执行
14      */
15 }
没缓存的 channel 使用 close 后,不会阻塞
1 func test9()  {
2     /** 编译通过 */
3     /** 演示,没缓存的 channel 使用 close 后,不会阻塞 */
4     done := make(chan bool)
5     close(done)
6     //done<-true  /** 关闭了的,不能再往里面输入值 */
7     <-done        /** 这句是读取,但是在上面已经关闭 channel 了,不会阻死 */
8     println("完成")
9 }
没缓存的 channel,在 go routine 里面使用 close 后,不会阻塞
 1 func test10()  {
 2     /** 编译通过 */
 3     /** 演示,没缓存的 channel,在 go routine 里面使用 close 后,不会阻塞 */
 4     done := make(chan bool)
 5     go func() {
 6         close(done)
 7     }()
 8     //done<-true  /** 关闭了的,不能再往里面输入值 */
 9     <-done        /** 这句是读取,但是在上面已经关闭 channel 了,不会阻死 */
10     println("完成")
11 }
有缓冲的 channel 不会阻塞的例子
1 func test11()  {
2     /** 编译通过 */
3     /** 有缓冲的 channel 不会阻塞的例子 */
4     done := make(chan bool,1)
5     done<-true
6     <-done
7     println("完成")
8 }
有缓冲的 channel 会阻塞的例子
1 func test12()  {
2     /** 编译通过 */
3     /** 有缓冲的 channel 会阻塞的例子 */
4     done := make(chan bool,1)
5     // done<-true /** 注释这句 */
6     <-done /** 虽然是有缓冲的,但是在没输入的情况下,读取,会阻塞 */
7     println("完成")
8 }
有缓冲的 channel 会阻塞的例子
1 func test13()  {
2     /** 编译不通过 */
3     /** 有缓冲的 channel 会阻塞的例子 */
4     done := make(chan bool,1)
5     done<-true
6     done<-false /** 放第二个值的时候,第一个还没被人拿走,这时候才会阻塞,根据缓冲值而定 */
7     println("完成")
8 }
有缓冲的 channel 不会阻塞的例子
1 func test14()  {
2     /** 编译通过 */
3     /** 有缓冲的 channel 不会阻塞的例子 */
4     done := make(chan bool,1)
5     done<-true   /** 不会阻塞在这里,等待读取 */
6 
7     println("完成")
8 }
有缓冲的channel,如果在 go routine 中使用,一定要做适当的延时,否则会输出来不及,因为main已经跑完了,所以延时一会,等待 go routine
 1 func test15()  {
 2     /** 编译通过 */
 3     /** 有缓冲的channel 在 go routine 里面的例子 */
 4     done := make(chan bool,1)
 5     go func() {
 6         /** 不会阻塞 */
 7         println("我可能会输出哦")
 8         done<-true  /** 如果把这个注释,也会导致 <-done 阻塞 */
 9         println("我也可能会输出哦")
10         <-done
11         println("别注释 done<-true 哦,不然我就输出不了了")
12     }()
13     time.Sleep(time.Second * 1)  /** 1秒延时,去掉就可能上面的都不会输出也有可以输出,routine 调度 */
14     println("完成")
15     /**
16      * 控制台输出:
17      *       我可能会输出哦
18      *       我也可能会输出哦
19      *       完成
20      */
21     /**
22      * 结论:
23      *    有缓冲的channel,如果在 go routine 中使用,一定要做适当的延时,否则会输出来不及,
24      *    因为main已经跑完了,所以延时一会,等待 go routine
25      */
26 }
多channel模式
 1 func getMessagesChannel(msg string, delay time.Duration) <-chan string {
 2     c := make(chan string)
 3     go func() {
 4         for i := 1; i <= 3; i++ {
 5             c <- fmt.Sprintf("%s %d", msg, i)
 6             time.Sleep(time.Millisecond * delay) /** 仅仅起到,下一次的 c 在何时输入 */
 7         }
 8     }()
 9     return c
10 }
11 
12 func test16()  {
13     /** 编译通过 */
14     /** 复杂的演示例子 */
15     /** 多channel模式 */
16     c1 := getMessagesChannel("第一", 600 )
17     c2 := getMessagesChannel("第二", 500 )
18     c3 := getMessagesChannel("第三", 5000)
19 
20     /** 层层限制阻塞 */
21     /** 这个 for 里面会造成等待输入,c1 会阻塞 c2 ,c2 阻塞 c3 */
22     /** 所以它总是,先输出 c1 然后是 c2 最后是 c3 */
23     for i := 1; i <= 3; i++ {
24         /** 每次循环提取一轮,共三轮 */
25         println(<-c1)  /** 除非 c1 有输入值,否则就阻塞下面的 c2,c3 */
26         println(<-c2)  /** 除非 c2 有输入值,否则就阻塞下面的 c3 */
27         println(<-c3)  /** 除非 c3 有输入值,否则就阻塞进入下一轮循环,反复如此 */
28     }
29     /**
30      *  这个程序的运行结果,首轮的,第一,第二,第三 很快输出,因为
31      *  getMessagesChannel 函数的延时 在 输入值之后,在第二轮及其之后
32      *  因为下一个 c3 要等到 5秒后才能输入,所以会阻塞第二轮循环的开始5秒,如此反复。
33      */
34     /** 修改:如果把 getMessagesChannel 里面的延时,放在输入值之前,那么 c3 总是等待 5秒 后输出 */
35 }
在 test15 基础修改的,复杂演示例,多channel 的选择,延时在输入之后的情况
 1 func test17()  {
 2     /** 编译通过 */
 3     /** 在 test15 基础修改的,复杂演示例子 */
 4     /** 多channel 的选择,延时在输入之后的情况 */
 5     c1 := getMessagesChannel("第一", 600 )
 6     c2 := getMessagesChannel("第二", 500 )
 7     c3 := getMessagesChannel("第三", 5000)
 8     /** 3x3 次循环,是 9 */
 9     /** select 总是会把最先完成输入的channel输出,而且,互不限制 */
10     /** c1,c2,c3 每两个互不限制 */
11     for i := 1; i <= 9; i++ {
12         select {
13         case msg := <-c1:
14             println(msg)
15         case msg := <-c2:
16             println(msg)
17         case msg := <-c3:
18             println(msg)
19         }
20     }
21     /**
22      * 这个程序的运行结果:
23      *    第二 1,第三 1,第一 1,第二 2,第一 2,第二 3,第一 3,第三 2,第三 3
24      */
25     /** 分析:前3次输出,“第一”,“第二”,“第三”,都有,而且
26      *  是随机顺序输出,因为协程的调度,第4,5,6次,由于“第二”只延时 500ms,
27      *  比 600ms 和 5000ms 都要小,那么它先输出,然后是“第一”,此时“第三”还不能输出,
28      *  因为它还在等5秒。此时已经输出5次,再过 500ms,"第三"的5秒还没走完,所以继续输出"第一",
29      *  再过 100ms,500+100=600,"第二"也再完成了一次,那么输出。至此,"第一"和"第二"已经
30      *  把管道的 3 个值全部输出,9-7 = 2,剩下两个是 "第三"。此时,距离首次的 5000ms 完成,
31      *  还有,500-600-600 = 3800ms,达到后,"第三" 将输出,再过5秒,最后一次"第三输出"
32      */
33 }

欢迎转载

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏日常分享

JSP/Servlet Web 学习笔记 DayThree

  使用JSP语法可以存取这些内置对象来执行JSP网页的Servlet环境相互作用。内置对象其实是由特定的Java类所产生的。每一种内置对象都映射到一个特定的J...

752
来自专栏企鹅号快讯

探索Android架构组件Room

文:栋栋 本文原创,转载请注明作者及出处 一、简介 Room是Google推出的Android架构组件库中的数据持久化组件库, 也可以说是在SQLite上实现的...

2945
来自专栏哲学驱动设计

C# async/await 使用总结

今天搞这两个关键字搞得有点晕,主要还是没有彻底理解其中的原理。 混淆了一个调用异步方法的概念: 在调用异步方法时,虽然方法返回一个 Task,但是其中的代码已经...

1816
来自专栏大内老A

ASP.NET Core真实管道详解[2]:Server是如何完成针对请求的监听、接收与响应的【上】

Server是ASP .NET Core管道的第一个节点,负责完整请求的监听和接收,最终对请求的响应同样也由它完成。Server是我们对所有实现了IServer...

2585
来自专栏转载gongluck的CSDN博客

Boost asio 官方教程

7.1. 概述 本章介绍了 Boost C++ 库 Asio,它是异步输入输出的核心。 名字本身就说明了一切:Asio 意即异步输入/输出。 该库可以让 ...

4655
来自专栏linux驱动个人学习

Linux进程ID号--Linux进程的管理与调度(三)【转】

Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,...

581
来自专栏跟着阿笨一起玩NET

多线程什么时候该加锁?

1 .加锁、解锁(同步/互斥)是多线程中非常基本的操作,但我却看到不少的代码对它们处理的很不好。简单说来有三类问题,一是加锁范围太大,虽然避免了逻辑错误,但锁了...

481
来自专栏Java Edge

JMM1、基础与概念2、重排序6、锁7、java concurrent包的通用化的实现模式7、final8、双重检查和延迟优化

2829
来自专栏编程之旅

2.24

oc中的内存管理机制:使用一种叫做引用计数的机制来管理内存中的对象。OC中每个对象都对应着他的引用计数,引用计数可以理解为一个整数计数器,当使用alloc方法创...

743
来自专栏大内老A

WCF服务端运行时架构体系详解[下篇]

作为WCF中一个核心概念,终结点在不同的语境中实际上指代不同的对象。站在服务描述的角度,我们所说的终结点实际上是指ServiceEndpoint对象。如果站在W...

1607

扫码关注云+社区