如何退出协程 goroutine (其他场景)
Go 语言/golang 高性能编程,Go 语言进阶教程,Go 语言高性能编程(high performance go)。本文介绍了协程没有正常关闭导致内存泄漏的场景,并介绍了如何借助通道/信道(channel) 优雅地退出协程。
Last updated
Go 语言/golang 高性能编程,Go 语言进阶教程,Go 语言高性能编程(high performance go)。本文介绍了协程没有正常关闭导致内存泄漏的场景,并介绍了如何借助通道/信道(channel) 优雅地退出协程。
Last updated
我们在 如何退出协程(超时场景) 这篇文章中举了一个因超时协程不能正常退出的例子。事实上除了超时场景,其他使用协程(goroutine)的场景,也很容易因为实现不当,导致协程无法退出,随着时间的积累,造成内存耗尽,程序崩溃。
例如下面的例子:
do
的实现非常简单,for + select 的模式,等待信道 taskCh 传递任务,并执行。
sendTasks
模拟向信道中发送任务。
该用例执行结果如下:
单元测试执行结束后,子协程多了一个,也就是说,有一个协程一直没有得到释放。我们仔细看代码,很容易发现 sendTasks
中启动了一个子协程 go do(taskCh)
,因为这个协程一直处于阻塞状态,等待接收任务,因此直到程序结束,协程也没有释放。
如果任务全部发送成功,我们如何通知该协程结束等待,正常退出呢?
创建 channel
关闭 channel
向通道发送值 v
从通道中接收值
接收操作可以有 2 个返回值。
beforeClosed 代表 v 是否是信道关闭前发送的。true 代表是信道关闭前发送的,false 代表信道已经关闭。如果一个信道已经关闭,<-ch
将永远不会发生阻塞,但是我们可以通过第二个返回值 beforeClosed 得知信道已经关闭,作出相应的处理。
与其他容器类型一致,支持查询长度和容量
关闭
panic
panic
成功关闭
发送数据
永久阻塞
panic
阻塞或成功发送
接收数据
永久阻塞
永不阻塞
阻塞或者成功接收
两个地方修改下即可:
t, beforeClosed := <-taskCh
判断 channel 是否已经关闭,beforeClosed 为 false 表示信道已被关闭。若关闭,则不再阻塞等待,直接返回,对应的协程随之退出。
sendTasks
函数中,任务发送结束之后,使用 close(taskCh)
将 channel taskCh 关闭。
测试用例执行结果如下:
可以发现,启动的协程已经正常退出,该协程以及使用到的信道 taskCh 将被垃圾回收,资源得到释放。
关于通道和协程的垃圾回收
注意,一个通道被其发送数据协程队列和接收数据协程队列中的所有协程引用着。因此,如果一个通道的这两个队列只要有一个不为空,则此通道肯定不会被垃圾回收。另一方面,如果一个协程处于一个通道的某个协程队列之中,则此协程也肯定不会被垃圾回收,即使此通道仅被此协程所引用。事实上,一个协程只有在退出后才能被垃圾回收。
-- 通道 - go101
通道关闭原则
一个常用的使用Go通道的原则是不要在数据接收方或者在有多个发送者的情况下关闭通道。换句话说,我们只应该让一个通道唯一的发送者关闭此通道。
在 如何优雅地关闭通道 - go101 这篇文章中,作者介绍了常见的几种关闭 channel 的方法:
如果 channel 已经被关闭,再次关闭会产生 panic,这时通过 recover 使程序恢复正常。
使用 sync.Once 或互斥锁(sync.Mutex)确保 channel 只被关闭一次。
情形一:M个接收者和一个发送者,发送者通过关闭用来传输数据的通道来传递发送结束信号。
情形二:一个接收者和N个发送者,此唯一接收者通过关闭一个额外的信号通道来通知发送者不要再发送数据了。
情形三:M个接收者和N个发送者,它们中的任何协程都可以让一个中间调解协程帮忙发出停止数据传送的信号。
详细的实现可以查看原文,在这里就不一一列举了~