如何优雅的关闭channel?
一、channel使用存在的不方便地方
1、在不改变channel自身状态的情况下,无法获知一个channnel是否关闭。
2、关闭一个已经关闭的channel,会导致panic。因此,如果关闭channel的一方在不知道channel是否关闭状态时就去贸然关闭channel是件很危险的事。
3、向一个已经关闭的channel发送数据会导致panic。因此,如果向channel发送数据的一方不知道channel是否处于关闭状态就贸然向channel发送数据是很危险的事情。
一个比较粗糙的检查channel是否关闭的函数:
func IsClosed(ch <-chan interface{}) bool {
select {
case <-ch:
return true
default:
}
return false
}
func main() {
c := make(chan interface{})
fmt.Println(IsClosed(c)) // false
close(c)
fmt.Println(IsClosed(c)) // true
}
上面的代码其实存在很多问题。
首先,IsClosed函数是一个有副作用的函数。每调用一次,都会读出channel里面的一个元素,改变了channel的状态,这不是一个好函数,干活就干活,还顺手牵羊?
其次,IsClosed函数返回的结果仅代表调用时候的那个瞬间,并不能保证调用之后不会有其他goroutine对这个channel进行了一些操作,改变了这个channel的状态。比如:IsClosed函数返回true,但这时有另外一个goroutine关闭了这个channel,这时候我们就会拿着这个过时的"channel未关闭"信息,向其发送数据,就会导致panic的发生。
当然,一个channel不会被重复关闭两次,如果IsClosed函数返回的结果是true,说明channel是真的关闭了。
有一句广泛流传的关闭channel的原则:
不要从一个 receiver 侧关闭 channel,也不要在有多个 sender 时,关闭 channel。
向channel发送元素的就是sender,因此sender可以决定何时不发送数据,并且关闭channel。但是如果有多个sender,某个sender同样没法确定其他sender的情况。这时候也不能贸然的关闭channel。
有两个不优雅关闭channel的方法:
1、使用defer-recover机制,放心大胆地关闭channel或者向channel发送数据,即使发生了panic,有defer-recover在兜底。
2、使用sync.Once来保证只关闭一次。
二、如何优雅关闭channel
根据sender和receiver的个数,可以分为以下几种情况:
1、一个sender,一个receiver
2、一个sender,M个receiver
3、N个sender,一个receiver
4、N个sender,M个receiver
对于1,2这两种情况,只有一个sender,直接从sender段关闭就好。
第3种情形下,优雅关闭channel的方法是:唯一的receiver发出一个关闭channel的信号,senders监听到关闭信号后,停止发送数据。
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 10000
const NumSenders = 1000
dataChan := make(chan int, 100)
stopChan := make(chan struct{})
// senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
select {
// 监听关闭channel的信号,退出
case <-stopChan:
return
// 给dataChan中发送数据
case dataChan <- rand.Intn(Max):
}
}
}()
}
// receiver
go func() {
// 从dataChan中遍历数据
for value := range dataChan {
// 如果value的值为9999,通知sender停止发送数据
if value == Max-1 {
fmt.Println("send stop signal to senders.")
close(stopChan)
return
}
fmt.Println(value)
}
}()
select {
// 阻塞一小时退出
case <-time.After(time.Hour):
}
}
这里的stopCh就是信号channel,它本身只有一个sender,因此可以直接关闭它。senders收到了关闭信号后,select分支case<-stopCh被选中,退出函数,不再发送数据。
需要说明的是,上面的代码并没有明确关闭dataCh。在Go语言中,对于一个channel,如果最终没有任何goroutine引用它,不管channel有没有被关闭,最终都会被GC回收。所以在这种情形下,所谓的优雅地关闭channel就是不关闭channel,让GC代劳。
最后一种情况,优雅关闭channel的方法是:any one of them says “let’s end the game” by notifying a moderator to close an additional signal channel。
和第三种情况不同,这里有M个receiver,如果,采用第3种解决方案,由receiver直接关闭stopCh的话,就会重复关闭一个channel,导致panic。因此需要增加一个中间人,M个receiver都向它发送关闭dataCh的"请求",中间人收到第一个请求后,就会直接下达关闭dataCh的指令(通过关闭stopCh,这时就不会发生重复关闭的情况,因为stopCh的发送方只有中间人一个)。另外这里N个sender也可以向中间人发送关闭dataCh的请求。
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 10000
const NumReceivers = 10
const NumSenders = 1000
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// 这里必须是一个有缓冲的channel
toStop := make(chan string, 1)
var stoppedBy string
// 中间人
go func() {
// 接收到了关闭channel的请求
stoppedBy = <-toStop
// 发送关闭dataCh的信号
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
// 如果value=0,则给中间人发送关闭channel的信号
if value == 0 {
select {
case toStop <- "sender#" + id:
// 此处是为了防止toStop这个channel阻塞
default:
}
return
}
select {
// 监听关闭channel的信号
case <-stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
for {
select {
// 监听关闭信号,退出,停止接收数据
case <-stopCh:
return
// 接收数据
case value := <-dataCh:
// 如果接收到了9999,则给中间人发送关闭channel的信号
if value == Max-1 {
select {
case toStop <- "receiver#" + id:
default:
}
return
}
fmt.Println(value)
}
}
}(strconv.Itoa(i))
}
select {
case <-time.After(time.Hour):
}
}
代码里toStop就是中间人的角色,使用它来接收senders和receivers发送过来的关闭dataCh请求。
这里将toStop声明成了一个缓冲型的channel。假设toStop声明的是一个非缓冲型的channel,那么第一个发送的关闭dataCh请求可能会丢失。因为无论是sender还是receiver都是通过select语句来发送请求,如果中间人所在的goroutine没有准备好,那么select语句就不会被选中,直接走default选项了,什么都不做。这样,第一个关闭dataCh的请求就会丢失。
如果把toStop的容量声明成Num(senders) + Num(receivers),那么发送dataCh请求的部分可以改写成更简洁的形式:
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 10000
const NumReceivers = 10
const NumSenders = 1000
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// 这里必须是一个有缓冲的channel
toStop := make(chan string, NumSenders+NumReceivers)
var stoppedBy string
// 中间人
go func() {
// 接收到了关闭channel的请求
stoppedBy = <-toStop
// 发送关闭dataCh的信号
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
// 如果value=0,则给中间人发送关闭channel的信号
if value == 0 {
toStop <- "sender#" + id
return
}
select {
// 监听关闭channel的信号
case <-stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
for {
select {
// 监听关闭信号,退出,停止接收数据
case <-stopCh:
return
// 接收数据
case value := <-dataCh:
// 如果接收到了9999,则给中间人发送关闭channel的信号
if value == Max-1 {
toStop <- "receiver#" + id
return
}
fmt.Println(value)
}
}
}(strconv.Itoa(i))
}
select {
case <-time.After(time.Hour):
}
}
直接向toStop发送请求,因为toStop容量足够大,所以不用担心阻塞,自然也就不用select语句再加一个default case来避免阻塞。
可以看到,这里同样没有真正关闭dataCh,同第3种情况。
参考链接:
https://golang.design/go-questions/channel/graceful-close/
如何优雅的关闭channel?的更多相关文章
- 如何优雅的关闭Golang Channel?
Channel关闭原则 不要在消费端关闭channel,不要在有多个并行的生产者时对channel执行关闭操作. 也就是说应该只在[唯一的或者最后唯一剩下]的生产者协程中关闭channel,来通知消费 ...
- 如何优雅的关闭golang的channel
How to Gracefully Close Channels,这篇博客讲了如何优雅的关闭channel的技巧,好好研读,收获良多. 众所周知,在golang中,关闭或者向已关闭的channel发送 ...
- RabbitMQ阻塞读取时数据时,关闭channel引起的问题和解决方案
项目场景: 最近在项目中使用了RabbitMq,其中有一个功能必须能随时切断RabbitMq的coumser.第一时间写出来的代码如下: 伪代码: while(flag){ QueueingConsu ...
- 如何优雅的关闭Java线程池
面试中经常会问到,创建一个线程池需要哪些参数啊,线程池的工作原理啊,却很少会问到线程池如何安全关闭的. 也正是因为大家不是很关注这块,即便是工作三四年的人,也会有因为线程池关闭不合理,导致应用无法正常 ...
- Effective java 系列之更优雅的关闭资源-try-with-resources
背景: 在Java编程过程中,如果打开了外部资源(文件.数据库连接.网络连接等),我们必须在这些外部资源使用完毕后,手动关闭它们.因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制,如果我们不在 ...
- 更优雅地关闭资源 - try-with-resource及其异常抑制
原文:https://www.cnblogs.com/itZhy/p/7636615.html 一.背景 我们知道,在Java编程过程中,如果打开了外部资源(文件.数据库连接.网络连接等),我们必须在 ...
- go语言之进阶篇关闭channel
1.关闭channel package main import ( "fmt" ) func main() { ch := make(chan int) //创建一个无缓存chan ...
- Java进阶知识点:更优雅地关闭资源 - try-with-resource
一.背景 我们知道,在Java编程过程中,如果打开了外部资源(文件.数据库连接.网络连接等),我们必须在这些外部资源使用完毕后,手动关闭它们.因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制, ...
- Java进阶知识点3:更优雅地关闭资源 - try-with-resource及其异常抑制
一.背景 我们知道,在Java编程过程中,如果打开了外部资源(文件.数据库连接.网络连接等),我们必须在这些外部资源使用完毕后,手动关闭它们.因为外部资源不由JVM管理,无法享用JVM的垃圾回收机制, ...
- 如何优雅地关闭一个socket
最近在windows编程时需要考虑到“如何优雅地关闭一个socket”,查阅了一些资料,现将查到的相关资料做个汇编,希望能对后来者有所帮助(比较懒,所以英文资料没有翻译:-)) 1. 关闭Socket ...
随机推荐
- go 简单封装数学运算包
前言 我们在编写程序时,经常会遇到一些高精度的数学运算,这时候使用简单的运算符会造成精度的缺失. 这里引用了这个第三方包 https://github.com/shopspring/decimal 做 ...
- Joker 智能开发平台:低代码开发的革新力量
在软件开发领域,开发效率与灵活性始终是开发者们追求的核心目标.随着技术的迅猛发展,低代码开发平台逐渐成为行业焦点,而 Joker 智能开发平台凭借其卓越的性能和创新的功能,脱颖而出,为开发者们带来了前 ...
- 如何每5分钟、10分钟或15分钟运行一次Cron计划任务
一个cron job是一个在指定时间段执行的任务.这些任务可以按分钟.小时.月.日.周.日或这些的任何组合来安排运行. Cron作业一般用于自动化系统维护或管理,例如备份数据库或数据.用最新的安全补丁 ...
- Tomcat的优化(分别为操作系统优化(内核参数优化),Tomcat配置文件参数优化,Java虚拟机(JVM)调优)
Tomcat的优化 一.Tomcat 优化 Tomcat 配置文件参数优化 二.系统内核优化 三.Tomcat 配置 JVM 参 ...
- 【Linux】5.10 输入输出重定向
Shell 输入/输出重定向 大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回到您的终端.一个命令通常从一个叫标准输入的地方读取输入,默认情况下,这恰好是你的终端.同样,一个命令 ...
- CompletableFuture原理及应用场景详解
1.应用场景 现在我们打开各个APP上的一个页面,可能就需要涉及后端几十个服务的API调用,比如某宝.某个外卖APP上,下面是某个外卖APP的首页.首页上的页面展示会关联很多服务的API调用,如果使用 ...
- P5490 【模板】扫描线 & 矩形面积并 做题笔记
扫描线是一种很常用的 trick,用来计算矩形并周长.并面积.核心思路是使用标记永久化 + 线段树,直接引用朴素的做法,即从某一维度开始扫描并将经过的面积加和. 错误 upd 函数中的汇总不正确,要想 ...
- MySQL的limit优化2
一.底层原理 在 MySQL 8.0 中,当使用 LIMIT offset, count 进行分页查询时,如果 offset 非常大(例如 LIMIT 200000, 10),性能会显著下降. 这是因 ...
- C#/.NET/.NET Core技术前沿周刊 | 第 34 期(2025年4.7-4.13)
前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...
- python操作PC版微信,给指定好友发信息(键鼠操作和复制粘贴相关库)
主要用来"pyautogui"."pyperclip"两个模块 pyautogui 主要用于控制键盘和鼠标操作.详细参考https://blog.csdn.ne ...