Golang, 以17个简短代码片段,切底弄懂 channel 基础
(原创出处为本博客:http://www.cnblogs.com/linguanh/)
前序:
因为打算自己搞个基于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 }
欢迎转载
我的“区块链”技术书籍:《区块链以太坊DApp开发实战》
、支付宝收款码 https://www.cnblogs.com/linguanh/gallery/825997.html
微信:https://www.cnblogs.com/linguanh/gallery/image/321906.html
银行卡:6217007200076746554 , 林冠宏