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
函数了。
否则,应该充分考虑自己程序的需求,进行必要的修改。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。