定时器是进程规划自己在未来某一时刻接获通知的一种机制。本节介绍两种定时器:Timer
(到达指定时间触发且只触发一次)和 Ticker
(间隔特定时间触发)。
Timer
类型代表单次时间事件。当 Timer
到期时,当时的时间会被发送给 C (channel),除非 Timer
是被 AfterFunc
函数创建的。
注意:Timer
的实例必须通过 NewTimer
或 AfterFunc
获得。
类型定义如下:
type Timer struct {
C <-chan Time // The channel on which the time is delivered.
r runtimeTimer
}
C 已经解释了,我们看看 runtimeTimer
。它定义在 sleep.go 文件中,必须和 runtime
包中 time.go
文件中的 timer
必须保持一致:
type timer struct {
i int // heap index
// Timer wakes up at when, and then at when+period, ... (period > 0 only)
// each time calling f(now, arg) in the timer goroutine, so f must be
// a well-behaved function and not block.
when int64
period int64
f func(interface{}, uintptr)
arg interface{}
seq uintptr
}
我们通过 NewTimer()
来看这些字段都怎么赋值,是什么用途。
// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
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
}
在 when
表示的时间到时,会往 Timer.C 中发送当前时间。when
表示的时间是纳秒时间,正常通过 runtimeNano() + int64(d)
赋值。跟上一节中讲到的 now()
类似,runtimeNano()
也在 runtime
中实现(runtime · nanotime
):
clock_gettime
获取时钟值(这是 POSIX 时钟)。其中 clockid_t 时钟类型是 CLOCK_MONOTONIC,也就是不可设定的恒定态时钟。具体的是什么时间,SUSv3 规定始于未予规范的过去某一点,Linux 上,始于系统启动。clock_gettime
不存在,则使用精度差些的系统调用 gettimeofday
。f
参数的值是 sendTime
,定时器时间到时,会调用 f,并将 arg
和 seq
传给 f
。
因为 Timer
是一次性的,所以 period
保留默认值 0。
定时器的具体实现逻辑,都在 runtime
中的 time.go
中,它的实现,没有采用经典 Unix 间隔定时器 setitimer
系统调用,也没有 采用 POSIX 间隔式定时器(相关系统调用:timer_create
、timer_settime
和 timer_delete
),而是通过四叉树堆 (heep) 实现的(runtimeTimer
结构中的 i
字段,表示在堆中的索引)。通过构建一个最小堆,保证最快拿到到期了的定时器执行。定时器的执行,在专门的 goroutine
中进行的:go timerproc()
。有兴趣的同学,可以阅读 runtime/time.go
的源码。
通过 time.After
模拟超时:
c := make(chan int)
go func() {
// time.Sleep(1 * time.Second)
time.Sleep(3 * time.Second)
<-c
}()
select {
case c <- 1:
fmt.Println("channel...")
case <-time.After(2 * time.Second):
close(c)
fmt.Println("timeout...")
}
time.Stop
停止定时器 或 time.Reset
重置定时器
start := time.Now()
timer := time.AfterFunc(2*time.Second, func() {
fmt.Println("after func callback, elaspe:", time.Now().Sub(start))
})
time.Sleep(1 * time.Second)
// time.Sleep(3*time.Second)
// Reset 在 Timer 还未触发时返回 true;触发了或 Stop 了,返回 false
if timer.Reset(3 * time.Second) {
fmt.Println("timer has not trigger!")
} else {
fmt.Println("timer had expired or stop!")
}
time.Sleep(10 * time.Second)
// output:
// timer has not trigger!
// after func callback, elaspe: 4.00026461s
如果定时器还未触发,Stop
会将其移除,并返回 true;否则返回 false;后续再对该 Timer
调用 Stop
,直接返回 false。
Reset
会先调用 stopTimer
再调用 startTimer
,类似于废弃之前的定时器,重新启动一个定时器。返回值和 Stop
一样。
查看 runtime/time.go
文件中的 timeSleep
可知,Sleep
的是通过 Timer
实现的,把当前 goroutine 作为 arg
参数(getg()
)。
Ticker
和 Timer
类似,区别是:Ticker
中的 runtimeTimer
字段的 period
字段会赋值为 NewTicker(d Duration)
中的 d
,表示每间隔 d
纳秒,定时器就会触发一次。
除非程序终止前定时器一直需要触发,否则,不需要时应该调用 Ticker.Stop
来释放相关资源。
如果程序终止前需要定时器一直触发,可以使用更简单方便的 time.Tick
函数,因为 Ticker
实例隐藏起来了,因此,该函数启动的定时器无法停止。
在实际开发中,定时器用的较多的会是 Timer
,如模拟超时,而需要类似 Tiker
的功能时,可以使用实现了 cron spec
的库 cron,感兴趣的可以参考文章:《Go 语言版 crontab》。