golang channel详解和协程优雅退出
非缓冲chan,读写对称
非缓冲channel,要求一端读取,一端写入。channel大小为零,所以读写操作一定要匹配。
func main() {
nochan := make(chan int)
go func(ch chan int) {
data := <-ch
fmt.Println("receive data ", data)
}(nochan)
nochan <- 5
fmt.Println("send data ", 5)
}
我们启动了一个协程从channel中读取数据,在主协程中写入,程序的运行流程是主协程优先启动,运行到nochan<-5写入是阻塞,然后启动协程读取,从而完成协程间通信。
程序输出
receive data 5
send data 5
如果将启动协程的代码放在nochan<-5下边,这样会造成主协程阻塞,无法启动协程,一直挂起。
func main() {
nochan := make(chan int)
nochan <- 5
fmt.Println("send data ", 5)
go func(ch chan int) {
data := <-ch
fmt.Println("receive data ", data)
}(nochan)
}
上述代码在运行时golang会直接panic,日志输出dead lock警告。
我们可以通过go run -race 选项检测并运行,是可以看到主协程一直阻塞,子协程无法启动的。
WaitGroup 待时而动
func main() {
nochan := make(chan int)
waiter := &sync.WaitGroup{}
waiter.Add(2)
go func(ch chan int, wt *sync.WaitGroup) {
data := <-ch
fmt.Println("receive data ", data)
wt.Done()
}(nochan, waiter)
go func(ch chan int, wt *sync.WaitGroup) {
ch <- 5
fmt.Println("send data ", 5)
wt.Done()
}(nochan, waiter)
waiter.Wait()
}
通过waitgroup管理两个协程,主协程等待两个子协程退出。
receive data 5
send data 5
range 自动读取
使用range可以自动的从channel中读取,当channel被关闭时,for循环退出,否则一直挂起
func main() {
catchan := make(chan int, 2)
go func(ch chan int) {
for i := 0; i < 2; i++ {
ch <- i
fmt.Println("send data is ", i)
}
//不关闭close,主协程将无法range退出
close(ch)
fmt.Println("goroutine1 exited")
}(catchan)
for data := range catchan {
fmt.Println("receive data is ", data)
}
fmt.Println("main exited")
}
输出如下
receive data is 0
send data is 0
send data is 1
goroutine1 exited
receive data is 1
main exited
如果不写close(ch),主协程将一直挂起,编译会出现死锁panic。
可以通过go run -race 选项检查看到主协程一直挂起。
缓冲channel, 先进先出
非缓冲channel内部其实是一个加锁的队列,先进先出。先写入的数据优先读出来。
func main() {
catchan := make(chan int, 2)
go func(ch chan int) {
for i := 0; i < 2; i++ {
ch <- i
fmt.Println("send data is ", i)
}
}(catchan)
for i := 0; i < 2; i++ {
data := <-catchan
fmt.Println("receive data is ", data)
}
}
输出如下
send data is 0
send data is 1
receive data is 0
receive data is 1
主协程从catchan中读取数据,子协程先catchan中写数据。主协程运行到读取位置先阻塞,子协程启动后向catchan中写数据后,主协程继续读取。
如果将主协程的for循环卸载go启动子协程之前,会造成编译警告死锁,当然可以通过go run -race 查看到主协程一直挂起。
读取关闭的channel
从关闭的channel中读取数据,优先读出其中没有取出的数据,然后读出存储类型的空置。循环读取关闭的channel不会阻塞,会一直读取空值。可以通过读取结果的bool值判断该channel是否关闭。
func main() {
nochan := make(chan int)
go func(ch chan int) {
ch <- 100
fmt.Println("send data", 100)
close(ch)
fmt.Println("goroutine exit")
}(nochan)
data := <-nochan
fmt.Println("receive data is ", data)
//从关闭的
data, ok := <-nochan
if !ok {
fmt.Println("receive close chan")
fmt.Println("receive data is ", data)
}
fmt.Println("main exited")
}
输出如下
receive data is 100
send data 100
goroutine exit
receive close chan
receive data is 0
main exited
主协程运行到data := <- nochan阻塞,子协程启动后向ch中写入数据,并关闭ch,此时主协程继续执行,取出一个数据后,再次取出为空值,并且ok为false表示ch已经被关闭。
切忌重复关闭channel
重复关闭channel会导致panic
func main() {
nochan := make(chan int)
go func(ch chan int) {
close(ch)
fmt.Println("goroutine exit")
}(nochan)
data, ok := <-nochan
if !ok {
fmt.Println("receive close chan")
fmt.Println("receive data is ", data)
}
//二次关闭
close(nochan)
fmt.Println("main exited")
}
输出如下
goroutine exit
receive close chan
receive data is 0
panic: close of closed channel
子协程退出后,主协程读取到退出信息,主协程再次关闭chan导致主协程崩溃。
切忌向关闭的channel写数据
向关闭的channel写数据会导致panic
func main() {
nochan := make(chan int)
go func(ch chan int) {
close(ch)
fmt.Println("goroutine1 exit")
}(nochan)
data, ok := <-nochan
if !ok {
fmt.Println("receive close chan")
fmt.Println("receive data is ", data)
}
go func(ch chan int) {
<-ch
fmt.Println("goroutine2 exit")
}(nochan)
//向关闭的channel中写数据
nochan <- 200
fmt.Println("main exited")
}
主线程运行到nochan读取数据阻塞,此时子协程1关闭,主协程继续执行获知nochan被关闭,然后启动子协程2,继续运行nochan<-200,此时nochan已被关闭,导致panic,效果如下
receive close chan
receive data is 0
goroutine1 exit
goroutine2 exit
panic: send on closed channel
切忌关闭nil的channel
关闭nil值的channel会导致panic
func main() {
var nochan chan int = nil
go func(ch chan int) {
//关闭nil channel会panic
close(ch)
fmt.Println("goroutine exit")
}(nochan)
//从nil channel中读取会阻塞
data, ok := <-nochan
if !ok {
fmt.Println("receive close chan")
fmt.Println("receive data is ", data)
}
fmt.Println("main exited")
}
主协程定义了一个nil值的nochan,并未开辟空间。运行至data, ok := <-nochan 阻塞,此时启动子协程,关闭nochan,导致panic
效果如下
panic: close of nil channel
读或写nil的channel都会阻塞
向nil的channel写数据,或者读取nil的channel也会导致阻塞。
func main() {
var nochan chan int = nil
go func(ch chan int) {
fmt.Println("goroutine begin receive data")
data, ok := <-nochan
if !ok {
fmt.Println("receive close chan")
}
fmt.Println("receive data is ", data)
fmt.Println("goroutine exit")
}(nochan)
fmt.Println("main begin send data")
//向nil channel中写数据会阻塞
nochan <- 100
fmt.Println("main exited")
}
如果直接编译系统会判断死锁panic,我们用go run -race main.go死锁检测,并运行,看到主协程一直挂起,子协程也一直挂起。
结果如下
goroutine begin receive data
main begin send data
主协程和子协程都阻塞了,一直挂起。
select 多路复用,大巧不工
select 内部可以写多个协程读写,通过case完成多路复用,其结构如下
select {
case ch <- 100:
...
case <- ch2:
...
dafault:
...
}
如果有多个case满足条件,则select随机选择一个执行。否则进入dafault执行。
我们可以利用上面的九种原理配合select创造出各种并发场景。
总结
1 当我们不使用一个channel时将其置为nil,这样select就不会检测它了。
2 当多个子协程想获取主协程退出通知时,可以从同一个chan中读取,如果主协程退出则关闭这个chan,那么所有从chan读取的子协程就会获得退出消息。从而实现广播。
3 为保证协程优雅退出,关闭channel的操作尽量放在对channel执行写操作的协程中。
并发实战
假设有这样的需求:
1 主协程启动两个协程,协程1负责发送数据给协程2,协程2负责接收并累加获得的数据。
2 主协程等待两个子协程退出,当主协程意外退出时通知两个子协程退出。
3 当发送协程崩溃和主动退出时通知接收协程也要退出,然后主协程退出
4 当接收协程崩溃或主动退出时通知发送协程退出,然后主协程退出。
5 无论三个协程主动退出还是panic,都要保证所有资源手动回收。
下面我们用上面总结的十招完成这个需求
datachan := make(chan int)
groutineclose := make(chan struct{})
mainclose := make(chan struct{})
var onceclose sync.Once
var readclose sync.Once
var sendclose sync.Once
var waitgroup sync.WaitGroup
waitgroup.Add(2)
datachan: 用来装载发送协程给接收协程的数据
groutineclose: 用于发送协程和接收协程之间关闭通知
onceclose: 保证datachan一次关闭。
readclose: 保证接收协程资源一次回收。
sendclose: 保证发送协程资源一次回收。
waitgroup: 主协程管理两个子协程。
接下来我们实现发送协程
go func(datachan chan int, gclose chan struct{}, mclose chan struct{}, group *sync.WaitGroup) {
defer func() {
onceclose.Do(func() {
close(gclose)
})
sendclose.Do(func() {
close(datachan)
fmt.Println("send goroutine closed !")
group.Done()
})
}()
for i := 0; i < 100; i++ {
select {
case <-gclose:
fmt.Println("other goroutine exited")
return
case <-mclose:
fmt.Println("main goroutine exited")
return
/*
default:
datachan <- i
*/
case datachan <- i:
}
}
}(datachan, groutineclose, mainclose, &waitgroup)
发送协程在defer函数中回收了和接收协程公用的chan,也主动关闭了数据chan,这么做保证关闭不会panic。此外还对group做了释放。
其实将datachan <- i 放在default分支也是可以的。但是为了保证接收协程退出后该发送协程也要及时退出,就放在case逻辑中,这样不会死锁。
发送协程累计发送100次数据给接收协程,然后退出。
接下来我们实现接收协程
go func(datachan chan int, gclose chan struct{}, mclose chan struct{}, group *sync.WaitGroup) {
sum := 0
defer func() {
onceclose.Do(func() {
close(gclose)
})
readclose.Do(func() {
fmt.Println("sum is ", sum)
fmt.Println("receive goroutine closed !")
group.Done()
})
}()
for i := 0; ; i++ {
select {
case <-gclose:
fmt.Println("other goroutine exited")
return
case <-mclose:
fmt.Println("main goroutine exited")
return
case data, ok := <-datachan:
if !ok {
fmt.Println("receive close chan data")
return
}
sum += data
}
}
}(datachan, groutineclose, mainclose, &waitgroup)
和发送协程一样,接收协程也通过once操作保证公用的通知chan只回收一次。然后回收了自己的资源。接收协程一直循环获取数据,如果收到主协程退出或者发送协程退出的通知,就退出。
接下来我们继续编写主协程的等待和回收操作
defer func() {
close(mainclose)
time.Sleep(time.Second * 5)
}()
waitgroup.Wait()
fmt.Println("main exited")
这些逻辑我们都写在main函数里即可。主协程通过waitgroup等待两个协程,并通过defer通知两个协程退出。
运行代码效果如下
send goroutine closed !
receive close chan data
sum is 4950
receive goroutine closed !
main exited
可以看出发送协程退出接收协程也退出了,接收协程正好计算100次累加,数值为4950。主协程也退出了。
测试接收协程异常退出
接下来我们测试接收协程异常退出后,发送协程和主协程退出是否回收资源。
我们将接收协程的case逻辑改为i>=20时该接收协程主动panic
case data, ok := <-datachan:
if !ok {
fmt.Println("receive close chan data")
return
}
sum += data
if i >= 20 {
panic("receive goroutine test panic !!")
}
运行代码看下效果
recover !
close gclose channel
sum is 210
receive goroutine closed !
other goroutine exited
send goroutine closed !
main exited
defer main close
我们在接收协程的defer里增加了recover逻辑,可以看到三个协程都正常退出并回收了各自的资源。
测试主协程主动退出
我们将主协程的等待代码去掉,并且在defer中增加延时退出,方便看到两个协程退出情况
defer func() {
fmt.Println("defer main close")
close(mainclose)
time.Sleep(time.Second * 10)
}()
time.Sleep(time.Second * 10)
fmt.Println("main exited")
运行看效果
main exited
defer main close
main goroutine exited
sum is 88074498378441
receive goroutine closed !
main goroutine exited
send goroutine closed !
看到三个协程正常退出,并回收了资源。
源码下载
https://github.com/secondtonone1/golang-/tree/master/channelpractice
我的公众号,谢谢关注

golang channel详解和协程优雅退出的更多相关文章
- python多进程详解和协程
1.由于python多线程适合于多IO操作,但不适合于cpu计算型工作,这时候可以通过多进程实现.python多进程简单实用 # 多进程,可以cpu保持一致,python多线程适合多io.对于高cpu ...
- Golang源码探索(二) 协程的实现原理
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻 ...
- Golang源码探索(二) 协程的实现原理(转)
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱,虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底 ...
- Go Channel 详解
原文链接:Go Channel 详解 Channel类型 Channel类型的定义格式如下: ChannelType = ( "chan" | "chan" & ...
- netty系列之:netty中的Channel详解
目录 简介 Channel详解 异步IO和ChannelFuture Channel的层级结构 释放资源 事件处理 总结 简介 Channel是连接ByteBuf和Event的桥梁,netty中的Ch ...
- 大数据学习day36-----flume02--------1.avro source和kafka source 2. 拦截器(Interceptor) 3. channel详解 4 sink 5 slector(选择器)6 sink processor
1.avro source和kafka source 1.1 avro source avro source是通过监听一个网络端口来收数据,而且接受的数据必须是使用avro序列化框架序列化后的数据.a ...
- go语言之行--golang核武器goroutine调度原理、channel详解
一.goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心.goroutine使用方式非常的简单,只需使用go关键字 ...
- [GO语言的并发之道] Goroutine调度原理&Channel详解
并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题:Go语言作为一个出道以来就自带 『高并发』光环的富二代编程语言,它的并发(并行)编程肯定是值得开发者去探究的,而Go ...
- golang 学习 (八)协程
一: 进程.线程 和 协程 之间概念的区别: 对于 进程.线程,都是有内核进行调度,有 CPU 时间片的概念,进行 抢占式调度(有多种调度算法) (补充: 抢占式调度与非抢占(轮询 ...
随机推荐
- SQLite3学习笔记(3)
SQLite 表达式 表达式是一个或多个值.运算符和计算值的 SQL函数的组合. SQL表达式与公式类似,都写在查询语言中.您还可以使用特定的数据集来查询数据库. SELECT语句的基本语法如下: S ...
- windows下如何打开.sketch的文件
1 .sketch的文件只能在苹果mac上支持的一种文件格式,现在越来越多的设计师喜欢用.sketch 2 windows下如果想打开.sketch文件,去Microsoft store 找一个Lun ...
- C# UdpClient使用
客户端: public class UdpClientManager { //接收数据事件 public Action<string> recvMessageEvent = null; / ...
- Linux之df磁盘信息
df命令用于查看磁盘的分区,磁盘已使用的空间,剩余的空间 1.用法 df [选项] [文件..] 2.命令选项 -a,--all 全部文件系统-h,--human-readable 以以合适的单位来显 ...
- GNU Radio下QT GUI Tab Widget的使用方法
期望显示出的效果: 即将要显示的图放在各自的标签页中. 整体框图: 具体设置: QT GUI Tab Widget的设置: 其中 ID改为自己想改的,这里我写的是display GUI Hint所代表 ...
- 【Linux】解决用vi修改文件,保存文件时,提示“readonly option is set”
当在终端执行sudo命令时,系统提示“hadoop is not in the sudoers file”: 其实就是没有权限进行sudo,解决方法如下(这里假设用户名是cuser): 1.切换到超级 ...
- TBDR下msaa 在metal vulkan和ogles的解决方案
https://developer.arm.com/solutions/graphics/developer-guides/understanding-render-passes/multi-samp ...
- C语言Ⅰ作业-05
这个作业属于哪个课程 C语言程序设计Ⅰ 这个作业要求在哪里 https://www.cnblogs.com/tongyingjun/p/11722665.html 我在这个课程的目标是 熟练掌握如何用 ...
- 利用collections下的counter实现对列表重复元素的查重
mylist = [0,1,1,2,2,3,3,3,3,4,5,6,7,7,8,8,9,10,10,11,22,33,22] from collections import Counter c = C ...
- jquery里把数组转换成json的方法
首先来看,jquery里自带的,和json相关的函数: 1.$.parseJSON : 用来解析JSON字符串,返回一个对象. 什么叫“JSON字符串”? 比如: var a={name:&quo ...