当前位置: 首页 > 科技观察

你应该掌握的高级Go并发模式:定时器

时间:2023-03-16 17:22:07 科技观察

前言如果你认为结合Goroutines来处理时间和计数器很容易,那你就错了,这里有一些与时间相关的问题或错误。定时器提到:时间:定时器。无法正确使用Reset#14038[1]时间:即使调用Timer.Reset后Timer.C仍然可以触发#11513[2]时间:记录Timer.Stop的正确用法#14383[3]阅读以上内容后链接内容,如果你还是觉得很简单,那么看看下面的代码,下面的代码会产生死锁和竞争条件tm:=time.NewTimer(1)tm.Reset(100*time.Millisecond)<-tm.Cif!tm.Stop(){<-tm.C}死锁代码片段functoChanTimed(t*time.Timer,chchanint){t.Reset(1*time.Second)deferfunc(){if!t.Stop(){<-t.C}}()select{casech<-42:case<-t.C:}}可能比较难理解,下面介绍相关方法。time.TickertypeTickerstruct{C<-chanTime//Thechannelonwhichtheticksaredelivered.}Ticker使用简单,但有一些小问题如果消息在C中已经存在,发送消息时会删除所有未读值。必须有stop操作:否则GC无法回收。设置C没有用:消息仍将在原始频道上发送。time.Ticktime.Tick是对time.NewTicker的封装。最好不要使用此方法,除非您计划返回chan作为结果并在程序的整个生命周期中继续使用它。正如官方描述的那样:垃圾收集器无法恢复底层的Ticker,出现“泄漏”。请谨慎使用,如有疑问,请改用Ticker。time.After这个和Tick基本是同一个概念,它封装了Timer。一旦计时器触发,它将被回收。请注意,计时器使用缓冲区大小为1的通道,即使没有接收器,它仍然可以计数。上面说了,如果你很在意性能,希望能够取消计时,那你就不应该使用After。time.Timer(也称为time.WhatTheFork?!)是Go的一个奇怪的API:NewTicker(Duration)返回一个*Timer类型,它只暴露一个定义为类型chan的变量C,这一点很奇怪。Go语言中通常可导出的字段是指用户可以获取或设置该字段,这里设置变量C没有实际意义。反之:设置C和重置Timer不会影响之前在C通道上的消息传递。更糟糕的是:AfterFunc返回的Timer根本不使用C。这样一来,Timer就很奇怪了,这里简单介绍一下API:Timer)Reset(dDuration)bool四个很简单的函数,其中两个是构造函数,难不成会出错?time.AfterFunc官方文档:在AfterFunc持续时间结束后,通过开启Goroutine调用f函数,返回一个Timer类型,从而通过Stop方法取消调用。虽然这样描述没有问题,但是需要注意的是,在调用Stop方法时,如果返回false,则说明函数已经执行完毕,stop失败。但并不代表函数已经返回,需要添加一些处理逻辑:done:=make(chanstruct{})f:=func(){doStuff()close(done)}t:=time.AfterFunc(1*time.Second,f)if!t.Stop(){<-done}这在Stop文档中有解释。否则,返回的计时器将不会被触发,只能用于调用Stop方法。t:=time.AfterFunc(1*time.Second,func(){fmt.Println("Timehaspassed!")})//Thiswilldeadlock.<-t.C还有,在写的时候,重置定时器会在Callf在传递给重置功能的时间段过去后再次出现,但此功能目前没有文档规范,将来可能会更改。time.NewTimer官方文档:NewTimer实例化一个Timer结构体,在durationd之后将当前时间发送到channel。这意味着不声明它就不可能构造一个有效的Timer类型结构。如果需要构建一个供后续复用,可以使用该方法实例化,或者使用下面的代码创建和停止计数器t:=time.NewTimer(0)if!t.Stop(){<-t.C}你必须从通道中读取数据。如果定时器在New和Stop调用期间被触发,并且通道中有未消费的数据,那么C就会有值。会导致后续读取错误。(*time.Timer).StopStop方法可防止计时器触发。如果调用停止计时器的方法,则返回true,如果计时器超时或停止,则返回false。上面这句话中的“或”很重要。文档中的所有Stop示例均显示以下代码片段:if!t.Stop(){<-t.C}关键点是“或”,表示有效0次或1次。对于通道数据已被消费且在此期间未多次调用Reset的情况无效。综上所述,当且仅当不消耗通道数据时,Stop+drain是安全的。在文档中体现如下:例如:假设程序还没有收到t.C的数据:另外,上述模式不是线程安全的,因为当通道数据被消费时,Stop返回的值可能已经过时,并且两个Goroutines试图消费通道C的数据也会导致死锁。(*time.Timer).Reset方法比较有趣,文档很长,你可以在这里查看文档中的一段有趣的摘录[4]:注意,因为清除通道和清除通道之间存在竞争条件使计数器过期,我们无法正确使用Reset返回值。Reset方法必须应用于已停止或过期的通道。文档提供的Reset的正确使用方法如下:if!t.Stop(){<-t.C}t.Reset(d)不能同时对来自channel的其他receiver使用Stop和Reset方法,在为了使传递给C的消息有效,应在每次重置之前使用C。重置计时器而不清除它会导致运行时丢弃该值,因为C缓存为1并且运行时有损发送到其他执行[5]。time.Timer:把这些方法放在一起Stop只有在New之后才是安全的,Reset方法只有在Stop之后才有效。只有在每次Stop运行后通道被消耗时,接收到的值才有效。仅当通道未被使用时才允许清除通道。下面是定时器转换、使用和调用关系的流程图:timer.png是定时器正确复用的例子,解决了文章开头提到的一些问题:functoChanTimed(t*time.Timer,chchanint){t.Reset(1*time.Second)//Nodefer,因为我们不知道哪个//case会被选择select{casech<-42:case<-t.C://Cisdrained,earlyreturnreturn}//我们将需要检查returnvalue//ofStop,因为etetcouldhavefired//betweenthesendonchandthisline。如果!t.Stop(){<-t.C}}上面的代码可以保证定时器在toChanTimed返回后可以被复用