Go语言中Timer计时器的使用技巧详解
作者:cainmusic
time包里有个Timer计时器的功能,主要的结构和函数有:
type Timer struct { C <-chan Time r runtimeTimer } func After(d Duration) <-chan Time func AfterFunc(d Duration, f func()) *Timer func NewTimer(d Duration) *Timer func (*Timer) Reset(d Duration) bool func (*Timer) Stop() bool
三个基本用法:
c := time.After(time.Second) fmt.Println(<-c) t := time.NewTimer(time.Second) fmt.Println(<-t.C) tc := make(chan int) time.AfterFunc(time.Second, func() { tc <- 1 }) fmt.Println(<-tc)
After
函数实际就是return NewTimer(d).C
,和NewTimer
的用法类似,但Timer
本身还有Reset
、Stop
等方法可用,有相关需求的,应使用NewTimer
。
AfterFunc
相当于在d Duration
之后创建了一个执行f
的goroutine,返回的Timer
本身并不会阻塞,也不能像前面的例子那样使用Timer.C
,但可以使用Reset
、Stop
等方法。
导致上面区别的原因在于使用NewTimer
和AfterFunc
生成计时器的时候,内部使用的调用参数并不相同。
NewTimer:
func NewTimer(d Duration) *Timer { c := make(chan Time, 1) t := &Timer{ C: c, r: runtimeTimer{ when: when(d), f: sendTime, arg: c, }, } startTimer(&t.r) return t } func sendTime(c interface{}, seq uintptr) { // Non-blocking send of time on c. // Used in NewTimer, it cannot block anyway (buffer). // Used in NewTicker, dropping sends on the floor is // the desired behavior when the reader gets behind, // because the sends are periodic. select { case c.(chan Time) <- Now(): default: } }
NewTimer
在计时器完成时使用sendTime
函数,非阻塞的向Timer.C
中传入当前时间,所以在计时器完成时,可以从其中获取内容。
AfterFunc:
func AfterFunc(d Duration, f func()) *Timer { t := &Timer{ r: runtimeTimer{ when: when(d), f: goFunc, arg: f, }, } startTimer(&t.r) return t } func goFunc(arg interface{}, seq uintptr) { go arg.(func())() }
AfterFunc
则是在计时器完成时调用goFunc
,在goFunc
中启动一个执行参数f
的goroutine,而并未对Timer.C
进行任何操作,于是我们无法从其中获取内容。
注:下面的内容主要基于NewTimer
创建的Timer
Timer
使用的关键点:
一,在一些任务中我们需要多次重复计时,不要使用循环创建大量计时器,会影响性能,尽量使用Reset
和Stop
来复用已创建的计时器。
二,Timer
的Stop
方法并不会关闭Timer.C
,可能会导致意外的阻塞,如:
func main() { timer := time.NewTimer(time.Second) go func() { timer.Stop() }() <-timer.C }
会导致程序阻塞,无法退出。
关于Timer
的Reset
和Stop
的使用小技巧:
// 用下面的非阻塞方法使用Stop func timerStop(t *time.Timer) { if !t.Stop() { select { case <-t.C: default: } } } // Reset之前先执行Stop func timerReset(t *time.Timer, d time.Duration) { timerStop(t) t.Reset(d) }
关于Reset
之前为何要Stop
,time
包的Reset
文档如下说:
For a Timer created with NewTimer, Reset should be invoked only on stopped or expired timers with drained channels.
对于使用NewTimer创建的Timer,Reset应该用在已经停止或过期,并已经排空管道的计时器上。
If a program has already received a value from t.C, the timer is known to have expired and the channel drained, so t.Reset can be used directly. If a program has not yet received a value from t.C, however, the timer must be stopped and—if Stop reports that the timer expired before being stopped—the channel explicitly drained:
如果一个程序已经从t.C中接收了值,计时器过期了并且管道已被排空,Reset可以直接使用。但如果程序还未从t.C中接收值,而计时器需要被停止,并且Stop方法报告计时器在被停止前已经过期,则管道需要被显式的排空:
if !t.Stop() { <-t.C } t.Reset(d)
This should not be done concurrent to other receives from the Timer's channel.
这个操作不应与其他程序接收计时器的管道同时发生。
注意,上面的内容其实还没表述完全。
如果我们需要停止一个计时器,并且计时器的Stop
方法报告为false
时,计时器的状态,以及t.C
的状态,共有三种可能:
- Stop前已经被Stop,t.C为空
- Stop前已经过期,计时器向t.C中写入内容,t.C为满
- Stop前已经过期,计时器向t.C中写入内容,t.C的信息已被其他程序接收,t.C为空
前面文档中的程序,仅在第2种情况会按照预期运行。
其他两种情况,显式排空<-t.C
的时候会阻塞。
就是因为上面的情况,才演化出前面的timerStop
函数。
但同时应该明白,timerStop
函数对应上面几种情况时如何处理:
- select走default分支,跳过阻塞,但应考虑到计时器并不是当前Stop停止的
- select进行显式排空,但应考虑到计时器并未被成功停止,并且t.C的内容被抛弃了
- select走default分支,跳过阻塞,但应考虑到计时器并未被成功停止,并且t.C的内容被其他程序利用了
充分的考虑到上面这几点,就可以使用timerStop
函数了。
否则,应该充分考虑自己程序的需求,进行必要的修改。
到此这篇关于Go语言中Timer计时器的使用技巧详解的文章就介绍到这了,更多相关Go Timer计时器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!