Go 如何正确关闭通道
序言
Go 在通道这一块,没有内置函数判断通道是否已经关闭,也没有可以直接获取当前通道数量的方法。所以对于通道,Go 显示的不是那么优雅。另外,如果对通道进行了错误的使用,将会直接引发系统 panic,这是一件很危险的事情。
如何判断通道是否关闭
虽然没有判断通道是否关闭的内置函数,但是官方为我们提供了一种语法来判断通道是否关闭:
v, ok := <-ch
// 如果ok为true则代表通道已经关闭
利用这个语法,我们可以编写这样的代码判断通道是否关闭:
func TestChanClosed(t *testing.T) {
var ch = make(chan int)
// send
go func() {
for {
ch <- 1
}
}()
// receive
go func() {
for {
if v, ok := <-ch; ok {
t.Log(v)
} else {
t.Log("通道关闭")
return
}
}
}()
time.Sleep(1 * time.Second)
}
也可以用 for range 简化语法,通道关闭后会主动退出 for 循环:
func TestChanClosed(t *testing.T) {
var ch = make(chan int)
// send
go func() {
for {
ch <- 1
}
}()
// receive
go func() {
for v := range ch {
t.Log(v)
}
t.Log("通道关闭")
return
}()
time.Sleep(1 * time.Second)
}
什么样的情况会 panic
有三种情况会引发 panic:
// 会引发channel panic的情况一:发送数据到已经关闭的channel
// panic: send on closed channel
func TestChannelPanic1(t *testing.T) {
var ch = make(chan int)
close(ch)
time.Sleep(10 * time.Millisecond)
go func() {
ch <- 1
}()
t.Log(<-ch)
}
// 会引发channel panic的情况一的另外一种:发送数据时关闭channel
// panic: send on closed channel
func TestChannelPanic11(t *testing.T) {
var ch = make(chan int)
go func() {
go func() {
// 没有接收数据的地方,此处会一直阻塞
ch <- 1
}()
}()
time.Sleep(20 * time.Millisecond)
close(ch)
}
// 会引发channel panic的情况二:重复关闭channel
// panic: close of closed channel
func TestChannelPanic2(t *testing.T) {
var ch = make(chan int)
close(ch)
close(ch)
}
// 会引发channel panic的情况三:未初始化关闭
// panic: close of nil channel
func TestChannelPanic3(t *testing.T) {
var ch chan int
close(ch)
}
我们在实际的业务中应该避免这三种不同的 panic,未初始化就关闭的情况较为少见,也不容易犯错误,重要的是要防止关闭后发送数据和重复关闭通道。
如何避免 panic
在 go 中有一条原则:Channel Closing Principle,它是指不要从接收端关闭 channel,也不要关闭有多个并发发送者的 channel。只要我们严格遵守这个原则,就可以有效的避免panic。其实这个原则就是让我们规避关闭后发送和重复关闭这两种情况。
为了应对关闭后发送数据这种情况,我们很容易想到Channel Closing Principle的第一句:不要从接收端关闭 channel。所以我们应该从发送端关闭 channel:
func TestSendClose(t *testing.T) {
var (
ch = make(chan int)
wg = sync.WaitGroup{}
// 10毫秒后通知发送端停止发送数据
after = time.After(10 * time.Millisecond)
)
wg.Add(2)
// send
go func() {
for {
select {
case <-after:
close(ch)
wg.Done()
return
default:
ch <- 1
}
}
}()
// receive
go func() {
defer wg.Done()
for v := range ch {
t.Log(v)
}
return
}()
wg.Wait()
}
这种方式可以应对单发送者的情况,如果我们的程序有多个发送者,那么就要考虑Channel Closing Principle的第二句话:不要关闭有多个并发发送者的 channel。那么这种情况下,我们应该如何正确的回收通道呢?这个时候我们可以考虑引入一个额外的通道,当接收端不想再接收数据时,就发送数据到这个额外的通道中,来通知所有的发送端退出:
func TestManySendAndOneReceive(t *testing.T) {
var (
sender = 3
wg = sync.WaitGroup{}
numCh = make(chan int)
stopCh = make(chan struct{})
// 10毫秒后通知发送端停止发送数据
after = time.After(10 * time.Millisecond)
)
wg.Add(1)
// send
for i := 0; i < sender; i++ {
go func() {
for {
select {
case <-stopCh:
fmt.Println("收到退出信号")
return
case numCh <- 1:
//fmt.Println("发送成功", value)
}
}
}()
}
// receive
go func() {
for {
select {
case v := <-numCh:
fmt.Println("接收到数据", v)
case <-after:
close(stopCh)
wg.Done()
return
}
}
}()
wg.Wait()
}
看完这段代码,我们发现 numCh 这个通道是没有关闭语句的,那么这段代码会引发内存泄漏吗?答案是不会,因为我们正确退出了发送端和接收端的所有协程,等到这个通道没有任何代码使用后,Go 的垃圾回收会回收此通道。
那如果此时我们的程序变得更为复杂:有多个接收者和多个发送者,这个时候怎么办呢?我们可以引入另外一个中间者,当任意协程想关闭的时候,都通知这个中间者,所有协程也同时监听这个中间者,收到中间者的退出信号时,退出当前协程:
func TestManySendAndManyReceive(t *testing.T) {
var (
maxRandomNumber = 5000
receiver = 10
sender = 10
wg = sync.WaitGroup{}
numCh = make(chan int)
stopCh = make(chan struct{})
toStop = make(chan string, 1)
stoppedBy string
)
wg.Add(receiver)
// moderator
go func() {
stoppedBy = <-toStop
close(stopCh)
}()
// senders
for i := 0; i < sender; i++ {
go func(id string) {
for {
value := rand.Intn(maxRandomNumber)
if value == 0 {
select {
case toStop <- "sender#" + id:
default:
}
return
}
// 提前关闭goroutine
select {
case <-stopCh:
return
default:
}
select {
case <-stopCh:
return
case numCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < receiver; i++ {
go func(id string) {
defer wg.Done()
for {
// 提前关闭goroutine
select {
case <-stopCh:
return
default:
}
select {
case <-stopCh:
return
case value := <-numCh:
if value == maxRandomNumber-1 {
select {
case toStop <- "receiver#" + id:
default:
}
return
}
t.Log(value)
}
}
}(strconv.Itoa(i))
}
wg.Wait()
t.Log("stopped by", stoppedBy)
}
避免重复关闭通道
可以使用 sync.once 语法来避免重复关闭通道:
type MyChannel struct {
C chan interface{}
once sync.Once
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan interface{})}
}
func (mc *MyChannel) SafeClose() {
mc.once.Do(func(){
close(mc.C)
})
}
也可以使用 sync.Mutex 语法避免重复关闭通道:
type MyChannel struct {
C chan interface{}
closed bool
mutex sync.Mutex
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan interface{})}
}
func (mc *MyChannel) SafeClose() {
mc.mutex.Lock()
if !mc.closed {
close(mc.C)
mc.closed = true
}
mc.mutex.Unlock()
}
func (mc *MyChannel) IsClosed() bool {
mc.mutex.Lock()
defer mc.mutex.Unlock()
return mc.closed
}
总结
如何正确关闭 gotoutine 和 channel 防止内存泄漏是一个重要的课题,如果在编码过程中,遇到了需要打破Channel Closing Principle原则的情况,一定要思考自己的代码设计是否合理。
Go 如何正确关闭通道的更多相关文章
- [JDBC]你真的会正确关闭connection吗?
Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = DriverManag ...
- 要恢复页面吗?Chrome未正确关闭
谷歌chrome浏览器每次打开提示"要恢复页面吗"怎么办? 谷歌chrome浏览器每次打开提示"要恢复页面吗"怎么办? 如下图所示: 每次打开启动谷歌chrom ...
- eclipse上一次没有正确关闭,导致启动的时候卡死错误解决方法
关于 eclipse启动卡死的问题(eclipse上一次没有正确关闭,导致启动的时候卡死错误解决方法),自己常用的解决方法: 方案一(推荐使用,如果没有这个文件,就使用方案二): 到<works ...
- golang 网络编程之如何正确关闭tcp连接以及管理它的生命周期
欢迎访问我的个人网站获取更佳阅读排版 golang 网络编程之如何正确关闭tcp连接以及管理它的生命周期 | yoko blog (https://pengrl.com/p/47401/) 本篇文章部 ...
- go语言从例子开始之Example29.关闭通道
关闭 一个通道意味着不能再向这个通道发送值了.这个特性可以用来给这个通道的接收方传达工作已经完成的信息. Example: package main import "fmt" // ...
- 线程池ExecutorService的使用及其正确关闭方法
创建一个容量为5的线程池 ExecutorService executorService = Executors.newFixedThreadPool(5); 向线程池提交15个任务,其实就是通过线程 ...
- 每次SSH执行完都会关闭通道,返回目录,如果想一次执行多步操作,需要多条命令才能达到目的时,用;分割操作指令,一并导入执行
每次SSH执行完都会关闭通道,返回目录,如果想一次执行多步操作,需要多条命令才能达到目的时,用:分割操作指令,一并导入执行: 例如: self.execmd='cd ../tmp/log/;pwd;t ...
- [翻译][Java]ExecutorService的正确关闭方法
https://blog.csdn.net/zaozi/article/details/38854561 https://blog.csdn.net/z69183787/article/details ...
- spring boot 服务 正确关闭方式
引言 Spring Boot,作为Spring框架对“约定优先于配置(Convention Over Configuration)”理念的最佳实践的产物,它能帮助我们很快捷的创建出独立运行.产品级别的 ...
- java-文件流正确关闭资源
用文件流来拷贝一个文件,用到文件字节输入流(FileInputStream)和文件字节输出流(FileOutputStream),用输入流把字节文件读到缓冲数组中,然后将缓冲数组中的字节写到文件中,就 ...
随机推荐
- 使用taro+canvas实现微信小程序的图片分享功能
业务场景 二轮充电业务中,用户充电完成后在订单详情页展示订单相关信息,用户点击分享按钮唤起微信小程序分享菜单,将生成的图片海报分享给微信好友或者下载到本地,好友可通过扫描海报中的二维码加群领取优惠. ...
- < Python全景系列-7 > 提升Python编程效率:模块与包全面解读
欢迎来到我们的系列博客<Python全景系列>!在这个系列中,我们将带领你从Python的基础知识开始,一步步深入到高级话题,帮助你掌握这门强大而灵活的编程语法.无论你是编程新手,还是有一 ...
- HashMap 底层是如何实现的?
HashMap 在不同的 JDK 版本下的实现是不同的,在 JDK 1.7 时,HashMap 底层是通过数组 + 链表实现的:而在 JDK 1.8 时,HashMap 底层是通过数组 + 链表或红黑 ...
- 手把手教你如何在 Linux 上源码安装最新版本 R
如果你使用的 Linux 系统 GCC 版本太低,又没有 root 权限(即使有 root 权限又担心升级 GCC 带来的风险):同时你又不想额外多安装多一个 Anaconda 或者 Minicond ...
- maven从远程仓库下载依赖包失败(因权限问题导致)
背景 在学习rocketMq时,编译官方提供的可视化项目:rocketmq-dashboard,频频失败,报以下错误 Could not transfer artifact org.apache.ro ...
- [ARM 汇编]高级部分—ARM汇编编程实战—3.3.1 嵌入式系统的基本概念
嵌入式系统是一种特殊的计算机系统,通常用于执行特定的任务.它通常包含一个或多个微处理器.存储器和外围设备.与通用计算机系统相比,嵌入式系统具有体积小.功耗低.成本低和实时性强等特点.在这一部分,我们将 ...
- mysql索引优化-01
1.1索引是什么? mysql官方对于索引的定义:可以帮助mysql高效的获取数据的数据结构. mysql在存储数据之外,数据库系统中还维护着满足特定查找算法的数据结构,这些数据结构给以某种引 ...
- 【Springboot】SpringBoot-Admin 服务监控+告警通知
SpringBoot-Admin 服务监控 简单介绍 Spring Boot Actuator 是 Spring Boot 自带的一个功能模块, 提供了一组已经开箱即用的生产环境下常用的特性和服务,比 ...
- PostgreSQL 10 文档: PostgreSQL 客户端工具
PostgreSQL 客户端应用 这部份包含PostgreSQL客户端应用和工具的参考信息.不是所有这些命令都是通用工具,某些需要特殊权限.这些应用的共同特征是它们可以被运行在任何主机上,而不管数 ...
- Linux 设置 VI 快捷键 -- 在多个打开的文件中切换
场景 部署完一系列服务后,想要查看所有服务的 catelina.out 日志: vi $(find /data/http | grep catalina.out | grep -v bak) 这个命令 ...