如何优雅的关闭Go Channel【译】

Golang 并发: goroutine and channel 前言 在前面的系列文章中,我们基本已经讲解完了 Golang 的数据类型、流程控制。函数、方法、接口、断言等基本知识点,这些内容其实和其他语言大同小异,除了语法以外并没有什么特别的地方。 那么,Go 语言和其他语言的区别是什么?她独特的魅力到底在哪里呢?答案就是并发。作为少有的语言层面就支持多协程的编程语言,Go 语言的并发编程简洁而优雅,并且拥有极为出色的性能。 本文先简单介绍一些并发的基本概念,在了解了 goroutine 协程之后,下一篇文章再探讨协程之间的通信。 文章目录前言进程、线程和协程进程线程 阅读详情

0?wx_fmt=png

Channel关闭原则

不要在消费端关闭channel,不要在有多个并行的生产者时对channel执行关闭操作。

也就是说应该只在[唯一的或者最后唯一剩下]的生产者协程中关闭channel,来通知消费者已经没有值可以继续读了。只要坚持这个原则,就可以确保向一个已经关闭的channel发送数据的情况不可能发生。

暴力关闭channel的正确方法

如果想要在消费端关闭channel,或者在多个生产者端关闭channel,可以使用recover机制来上个保险,避免程序因为panic而崩溃。

func SafeClose(ch chan T) (justClosed bool) {
	defer func() {
		if recover() != nil {
			justClosed = false
		}
	}()
	
	// assume ch != nil here.
	close(ch) // panic if ch is closed
	return true // <=> justClosed = true; return
}

使用这种方法明显违背了上面的channel关闭原则,然后性能还可以,毕竟在每个协程只会调用一次SafeClose,性能损失很小。

同样也可以在生产消息的时候使用recover方法。

func SafeSend(ch chan T, value T) (closed bool) {
	defer func() {
		if recover() != nil {
			// The return result can be altered 
			// in a defer function call.
			closed = true
		}
	}()
	
	ch <- value // panic if ch is closed
	return false // <=> closed = false; return
}

礼貌的关闭channel方法

还有不少人经常使用用sync.Once来关闭channel,这样可以确保只会关闭一次

type MyChannel struct {
	C    chan T
	once sync.Once
}

func NewMyChannel() *MyChannel {
	return &MyChannel{C: make(chan T)}
}

func (mc *MyChannel) SafeClose() {
	mc.once.Do(func() {
		close(mc.C)
	})
}

同样我们也可以使用sync.Mutex达到同样的目的。

type MyChannel struct {
	C      chan T
	closed bool
	mutex  sync.Mutex
}

func NewMyChannel() *MyChannel {
	return &MyChannel{C: make(chan T)}
}

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
}

要知道golang的设计者不提供SafeClose或者SafeSend方法是有原因的,他们本来就不推荐在消费端或者在并发的多个生产端关闭channel,比如关闭只读channel在语法上就彻底被禁止使用了。

优雅的关闭channel的方法

上文的SafeSend方法一个很大的劣势在于它不能用在select块的case语句中。而另一个很重要的劣势在于像我这样对代码有洁癖的人来说,使用panic/recover和sync/mutex来搞定不是那么的优雅。下面我们引入在不同的场景下可以使用的纯粹的优雅的解决方法。

多个消费者,单个生产者。这种情况最简单,直接让生产者关闭channel好了。

package main

import (
	"time"
	"math/rand"
	"sync"
	"log"
)

func main() {
	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)
	
	// ...
	const MaxRandomNumber = 100000
	const NumReceivers = 100
	
	wgReceivers := sync.WaitGroup{}
	wgReceivers.Add(NumReceivers)
	
	// ...
	dataCh := make(chan int, 100)
	
	// the sender
	go func() {
		for {
			if value := rand.Intn(MaxRandomNumber); value == 0 {
				// The only sender can close the channel safely.
				close(dataCh)
				return
			} else {			
				dataCh <- value
			}
		}
	}()
	
	// receivers
	for i := 0; i < NumReceivers; i++ {
		go func() {
			defer wgReceivers.Done()
			
			// Receive values until dataCh is closed and
			// the value buffer queue of dataCh is empty.
			for value := range dataCh {
				log.Println(value)
			}
		}()
	}
	
	wgReceivers.Wait()
}

多个生产者,单个消费者。这种情况要比上面的复杂一点。我们不能在消费端关闭channel,因为这违背了channel关闭原则。但是我们可以让消费端关闭一个附加的信号来通知发送端停止生产数据。

package main

import (
	"time"
	"math/rand"
	"sync"
	"log"
)

func main() {
	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)
	
	// ...
	const MaxRandomNumber = 100000
	const NumSenders = 1000
	
	wgReceivers := sync.WaitGroup{}
	wgReceivers.Add(1)
	
	// ...
	dataCh := make(chan int, 100)
	stopCh := make(chan struct{})
	// stopCh is an additional signal channel.
	// Its sender is the receiver of channel dataCh.
	// Its reveivers are the senders of channel dataCh.
	
	// senders
	for i := 0; i < NumSenders; i++ {
		go func() {
			for {
				// The first select here is to try to exit the goroutine
				// as early as possible. In fact, it is not essential
				// for this example, so it can be omitted.
				select {
				case <- stopCh:
					return
				default:
				}
				
				// Even if stopCh is closed, the first branch in the
				// second select may be still not selected for some
				// loops if the send to dataCh is also unblocked.
				// But this is acceptable, so the first select
				// can be omitted.
				select {
				case <- stopCh:
					return
				case dataCh <- rand.Intn(MaxRandomNumber):
				}
			}
		}()
	}
	
	// the receiver
	go func() {
		defer wgReceivers.Done()
		
		for value := range dataCh {
			if value == MaxRandomNumber-1 {
				// The receiver of the dataCh channel is
				// also the sender of the stopCh cahnnel.
				// It is safe to close the stop channel here.
				close(stopCh)
				return
			}
			
			log.Println(value)
		}
	}()
	
	// ...
	wgReceivers.Wait()
}

就上面这个例子,生产者同时也是退出信号channel的接受者,退出信号channel仍然是由它的生产端关闭的,所以这仍然没有违背channel关闭原则。值得注意的是,这个例子中生产端和接受端都没有关闭消息数据的channel,channel在没有任何goroutine引用的时候会自行关闭,而不需要显示进行关闭。

多个生产者,多个消费者

这是最复杂的一种情况,我们既不能让接受端也不能让发送端关闭channel。我们甚至都不能让接受者关闭一个退出信号来通知生产者停止生产。因为我们不能违反channel关闭原则。但是我们可以引入一个额外的协调者来关闭附加的退出信号channel。

package main

import (
	"time"
	"math/rand"
	"sync"
	"log"
	"strconv"
)

func main() {
	rand.Seed(time.Now().UnixNano())
	log.SetFlags(0)
	
	// ...
	const MaxRandomNumber = 100000
	const NumReceivers = 10
	const NumSenders = 1000
	
	wgReceivers := sync.WaitGroup{}
	wgReceivers.Add(NumReceivers)
	
	// ...
	dataCh := make(chan int, 100)
	stopCh := make(chan struct{})
		// stopCh is an additional signal channel.
		// Its sender is the moderator goroutine shown below.
		// Its reveivers are all senders and receivers of dataCh.
	toStop := make(chan string, 1)
		// The channel toStop is used to notify the moderator
		// to close the additional signal channel (stopCh).
		// Its senders are any senders and receivers of dataCh.
		// Its reveiver is the moderator goroutine shown below.
	
	var stoppedBy string
	
	// moderator
	go func() {
		stoppedBy = <- toStop
		close(stopCh)
	}()
	
	// senders
	for i := 0; i < NumSenders; i++ {
		go func(id string) {
			for {
				value := rand.Intn(MaxRandomNumber)
				if value == 0 {
					// Here, a trick is used to notify the moderator
					// to close the additional signal channel.
					select {
					case toStop <- "sender#" + id:
					default:
					}
					return
				}
				
				// The first select here is to try to exit the goroutine
				// as early as possible. This select blocks with one
				// receive operation case and one default branches will
				// be optimized as a try-receive operation by the
				// official Go compiler.
				select {
				case <- stopCh:
					return
				default:
				}
				
				// Even if stopCh is closed, the first branch in the
				// second select may be still not selected for some
				// loops (and for ever in theory) if the send to
				// dataCh is also unblocked.
				// This is why the first select block is needed.
				select {
				case <- stopCh:
					return
				case dataCh <- value:
				}
			}
		}(strconv.Itoa(i))
	}
	
	// receivers
	for i := 0; i < NumReceivers; i++ {
		go func(id string) {
			defer wgReceivers.Done()
			
			for {
				// Same as the sender goroutine, the first select here
				// is to try to exit the goroutine as early as possible.
				select {
				case <- stopCh:
					return
				default:
				}
				
				// Even if stopCh is closed, the first branch in the
				// second select may be still not selected for some
				// loops (and for ever in theory) if the receive from
				// dataCh is also unblocked.
				// This is why the first select block is needed.
				select {
				case <- stopCh:
					return
				case value := <-dataCh:
					if value == MaxRandomNumber-1 {
						// The same trick is used to notify
						// the moderator to close the
						// additional signal channel.
						select {
						case toStop <- "receiver#" + id:
						default:
						}
						return
					}
					
					log.Println(value)
				}
			}
		}(strconv.Itoa(i))
	}
	
	// ...
	wgReceivers.Wait()
	log.Println("stopped by", stoppedBy)
}

以上三种场景不能涵盖全部,但是它们是最常见最通用的三种场景,基本上所有的场景都可以划分为以上三类。

结论

没有任何场景值得你去打破channel关闭原则,如果你遇到这样的一种特殊场景,还是建议你好好思考一下自己设计,是不是该重构一下了。

0?wx_fmt=jpeg

APO的全面进化之路(2):走向IBP、建设端到端供应链计划体系 前一篇记录阐述了在供应链计划(Supply Chain Planning)领域里SAP的关键产品APO的不断进化之路,即部分产品从APO进化到S/4HANA,本篇将继续阐述进化之路上的另一大中坚产品力量SAP-IBP(Integrated Business Planning)。作为SAP数字化供应链(Digital Supply Chain)中供应链计划SCP的左膀右臂,S/4HANA与IBP共同完善了计划域的完整闭环。本篇作为收尾,在先谈对IBP产品的个人认知之前,特别是在当今这个IT产品迭代多、技术革新 阅读详情

相关推荐

DSP28335 ADC模块实战:从寄存器配置到精准数据采集(附代码详解)

本文详细解析DSP28335的ADC模块配置与数据采集实战,涵盖寄存器设置、采样窗口优化、中断处理及代码实现。重点解决ADC精度问题,如采样窗口不足导致数据偏差,提供完整代码示例和调试技巧,助力工程师实现精准数据采集。

kkk56的博客 866

GoLang之如何优雅地关闭 channel(8)

GoLang之如何优雅地关闭 channel(8)

weixin_52690231的博客 1537

深入解析时钟同步技术:相位同步、频率同步与同源时钟的实战应用

本文深入解析时钟同步技术的三种核心方式:相位同步、频率同步与同源时钟,详细阐述其技术原理、实现方案及实战应用。通过对比分析不同同步方式的特点与适用场景,为电子系统设计提供选型指南,帮助工程师优化时钟网络设计,提升系统稳定性和性能。

weixin_29240711的博客 451

Go面试看这里了~(十九)

原文地址:Go面试看这里了~(十九) 1、退出程序时怎么防止channel没有消费完? 不要在消费端关闭channel,不要在有多个并行的生产者时对channel执行关闭操作,应只在唯一或最后唯一剩下的生产者协程中关闭channel,来通知消费者已经无值可读。 对无缓冲channel来说,程序退出时将生产者关闭就不会产生多余数据给消费者。 对有缓冲channel最本质的原则只有一个,就是不要关闭或将值发送到已关闭通道。 可使用sync.Once来确保只会关闭一次channel,使用sync.Mut

luyaran的博客 1200

【视频讲解】Python量子计算聚类Q-means:量子k-means算法分析电路数据实现可视化...

全文链接:https://tecdat.cn/?p=37821分析师:Yifan Zhang量子计算在近期已然成为一个频繁出现的热门概念。尽管它在大众认知以及互联网社区中备受瞩目,热度极高,然而就其实际能力而言,当前仍然存在诸多局限(点击文末“阅读原文”获取完整代码数据)。量子计算作为一个全新的领域,带来了相较于传统经典计算模式的重大范式转变。在量子计算的体系中,传统的 0 或 1 的经典比特被一...

拓端研究室TRL 1456

电机控制——PMSM无感FOC控制(一)FOC入门

FOC 是一种电机控制技术,全称为 Field Oriented Control(磁场定向控制),也称作矢量控制。该技术可以提高电机的效率、控制稳定性和精度,广泛应用于电机驱动系统中,是目前无刷直流电机(BLDC)、永磁同步电机(PMSM)、感应电机的高效控制的最优方法之一。本节主要讲了FOC的大致流程,带大家先了解FOC到底是个什么东西在做什么,各个模块具体的原理会放到后面的章节详细讲解。FOC其实就是通过控制,相电流按照正弦变化,从而产生出旋转的磁场,控制电机转子转动。

zhuxinmingde的博客 2638

Go---channel实现生产者消费者

一、无缓冲区   package main // 无缓冲的channel import ( "fmt" "time" ) func produce(ch chan&lt;- int) { for i := 0; i &lt; 10; i++ { ch &lt;- i fmt.Println("Send:", i) } } func consumer(ch &lt;-c...

行者小朱的博客 9296

在 SAP ABAP 里可靠地对接 FTP 服务器:从连接、命令到传输与排错的完整实战

本文详细介绍了在SAP ABAP中实现FTP文件交换的完整技术方案。文章首先解析了FTP协议的双连接特性(控制连接与数据连接),强调保持控制连接的重要性。随后详细说明了ABAP中FTP操作的四步标准流程:建立连接(包含HTTP_SCRAMBLE加密)、执行命令、断开连接和关闭RFC通道。文章提供了实战案例代码,演示如何将FTP文件复制到应用服务器再读取进内表处理,特别指出后台作业与前台操作的关键差异。最后总结了常见错误排查要点,包括连接模型、路径设置和资源释放等问题。全文通过SAP标准函数模块的调用顺序、典

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 310

go面试题2020版

go面试题2020版 1、go的调度 2、go struct能不能比较 因为是强类型语言,所以不同类型的结构不能作比较,但是同一类型的实例值是可以比较的,实例不可以比较,因为是指针类型 3、go defer(for defer),先进后出,后进先出 func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } } 4、select可以用于什么,常用语gorotine的完美退出 golang 的 select 就是监听 IO 操作,当 IO 操作发生时

hell0_W0rld4396的博客 998

华为OD机试真题 新系统【优化充电桩调度算法】

华为OD机试 新系统真题【优化充电桩调度算法】,提供C语言、Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解,以及在线OJ模拟做题。华为OD机试真题【优化充电桩调度算法】

qq_45776114的博客 758

Openclaw错误排查及解决方案之:Something went wrong while processing your request. Please try again.

Openclaw错误排查及解决方案之:Something went wrong while processing your request. Please try again.

多媒体编程、网络编程、系统编程、网络安全编程、驱动编程 3792

如何优雅的关闭Go Channel「译」

Channel关闭原则 不要在消费端关闭channel,不要在有多个并行的生产者时对channel执行关闭操作。 也就是说应该只在[唯一的或者最后唯一剩下]的生产者协程中关闭channel,来通知消费者已经没有值可以继续读了。只要坚持这个原则,就可以确保向一个已经关闭的channel发送数据的情况不可能发生。 暴力关闭channel的正确方法 如果想要在消费端关闭channel,或者在多个生...

weixin_34414650的博客 171

【图论】系列(七)图论的中心性度量方法及重要性评价指标

图论中的中心性度量方法是用来衡量图中各个节点(或顶点)相对重要性的指标。

qq_30066733的博客 1078

有关Golang channel关闭的优雅方式

今天看到了一篇很不错的文章,读完之后我简单重写了示例代码,文字内容也不一致,有兴趣的同学可以去读一下原文或译文。 有关channel的问题 保证channel关闭一次的方式 关闭channel的优雅方式 一、有关channel的问题 Golang面试有关channel的问题肯可能会问到: Q:如何关闭一个channel A:close() Q:怎么判断一个channel已经关闭了 A:(1...

梅雨心情的博客 2507

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

GoLang之如何优雅地关闭 channel

因此需要增加一个中间人,M 个 receiver 都向它发送关闭 dataCh 的“请求”,中间人收到第一个请求后,就会直接下达关闭 dataCh 的指令(通过关闭 stopCh,这时就不会发生重复关闭的情况,因为 stopCh 的发送方只有中间人一个)。例如,IsClosed 函数返回 true,但这时有另一个 goroutine 关闭了 channel,而你还拿着这个过时的 “channel 未关闭”的信息,向其发送数据,就会导致 panic 的发生。这样,第一个关闭 dataCh 的请求就会丢失。

qq_53267860的博客 1963

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

怎么优雅地关闭Golang的Channel

本文介绍了Go的通道关闭原则。以及在几种常见场景下,如何在遵守此原则的前提下优雅地关闭通道。

夏日白云的boke 1715

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

如何优雅地关闭Go channel

如何优雅地关闭Go channel 天唯关注 0.82017.02.17 00:47*字数 1709阅读 17557评论 20喜欢 86 本文译自:How To Close Channels in Golang Elegantly。 几天前,我写了一篇文章来说明golang中channel的使用规范。在reddit和HN,那篇文章收到了很多赞同,但是我也收到了下面几个关于Go c...

jfkidear的专栏 427

如何优雅地关闭channel

如果没有任何数据被发送到(并且将被发送到)channel,那么确实有一种简单的方法可以检查channel是否关闭。应该有更多的情况变体,但上面显示的是最常见和最基本的变体。通过巧妙地使用channel(和其他并发编程技术),我相信总能找到一个针对每个情况变量的channel关闭原理的解决方案。这篇文章在reddit和HN上获得了很多肯定,但也有一些关于channel设计细节的批评。是的,确实没有内置函数来检查channel是否已经关闭。如果真的没法维持,请重新思考你的设计并重写你的代码。

小飞的博客 1098

如何优雅的关闭Go Channel

Channel关闭原则 不要再消费端关闭channel,不要在有多个并行的生产者时对Channel执行关闭操作 也就是说应该只在[唯一的或者最后唯一剩下的]生产者协程中关闭channel,来通知消费者已经没有值可以继续读了。 暴力的关闭channel正确方法 如果想要在消费端关闭channel,或者在多个生产者端关闭channel,可以使用recover机制来上个保险,避免程序因为panic崩溃。 消费端关闭 package main import "fmt" func main() { //va

weixin_44014995的博客 891

【GoLang】关于通道Channel 的关闭

在go中,channel的使用频率很高,那么你知道关于channel关闭的知识吗?

2201_75692232的博客 1725

如何 优雅的关闭 channel

在使用Go channel的时候,一个适用的原则是不要从接收端关闭channel,也不要在多个并发发送端中关闭channel。换句话说,如果sender(发送者)只是唯一的sender或者是channel最后一个活跃的sender,那么你应该在sender的goroutine关闭channel,从而通知receiver(s)(接收者们)已经没有值可以读了。维持这条原则将保证永远不会发生向一个已经关...

wudawei071193的博客 1222

go channel 基础(如何优雅地关闭 channel)

想必听说过 go 的,应该都知道 go 的最大的特性 goroutine 并发编程,而说到并发编程,使用 channel 进行数据传输是 go 中的必修课。 go 的并发哲学:不要通过共享内存来通信,而要通过通信来实现内存共享。 channel 的坑不少,本篇简单聊聊关闭 channel 的方法。

DisMisPres的博客 3322

golang channel 关闭之比优雅方案更优雅的方案

如果没了解过优雅方案,可以看一下这个简书如何优雅地关闭Go channel。 在我看来这些方案不优雅!不灵活!不实用!比如1个消费者N个生产者的情况下,如果要求消费者要把channel消费完该怎么处理? 要做到优雅,我觉得有3点: 消费者不关闭channel 消费者必须把channel内的数据消费完 channel必须在没有生产者使用时被关闭 我们只要做好第3点,第1第2点唾手可得。 ...

weixin_39216439的博客 684

Golang-如何优雅的关闭channel?

如何优雅的关闭channel? channel关闭原则 "如何优雅的关闭channel?",这种问题经常在面试题中出现,究其原因,是因为channel创建容易,但是关闭"不易",原因有以下三点: 关闭时机未知,在不改变channel自身状态的条件下,无法知道他是否已经关闭 不能无脑关闭,如果一个channel已经关闭,重复关闭channel会导致panic 往一个关闭的channel写数据,也...

Y先森0.0 1602

golang学习之如何判断Channel是否已经关闭【转载】

为什么有这样的问题? 来自我的一个bug,我期初认为close了一个channel,消费端的goroutine自然是可以拿到channel的关闭状态。然而事实并不是这样的。 只有当channel无数据,且channel被close了,才会返回ok=false。 所以,只要有堆积,就不会返回关闭状态。导致我的服务花时间来消费堆积,才会退出。 测试channel的关闭状态 // xiaorui.cc package main import ( "fmt" ) func m...

cyb_17302190874的博客 4165

优雅的关闭golang程序

优雅的关闭golang程序

一个运维开发工程师的博客 2958
上一篇: Java高阶必备之Netty基础原理
下一篇: Channel最佳实践之基本规则【译】
码洞
博客等级 码龄10年 41粉丝 137原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值