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计时器内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
