定时器是一种通过设置一项任务,在未来的某个时刻执行该任务的机制。
定时器的种类通常只有两种,一种是只执行一次的延时模式,一种是每隔一段时间执行一次的间隔模式。
在现代编程语言中,定时器几乎是标配。除了设置定时器外,还需要有提供定时器的方法。
比如在 JavaScript 中,提供了 setTimeout、setInterval、clearTimeout 和 clearInterval 四个 API,相比较而言是比较简单的。Go 语言中定时器的 API 就比较完善,所有的 API 都在 time 包中。
实际应用
延迟执行
延迟执行有两种方式,time.After 和 time.Sleep。
time.After
下面是输出 1 之后间隔 1 秒后再输出 2 的例子。
func main() { fmt.Println("1") timeAfterTrigger := time.After(1 * time.Second) <-timeAfterTrigger fmt.Println("2") }
其中 After 的参数 Duration 单位是纳秒。time 包中提供了运算好的几个 int 类型常量。
const ( Nanosecond Duration = 1 Microsecond = 1000 * Nanosecond Millisecond = 1000 * Microsecond Second = 1000 * Millisecond Minute = 60 * Second Hour = 60 * Minute )
time.Sleep
相比较而言,time.Sleep 实现同样的效果用起来就更加简洁了。
func main() { fmt.Println("1") time.Sleep(1 * time.Second) fmt.Println("2") }
两者的区别在于 time.Sleep 是阻塞当前协程,time.After 基于 channel 实现,可以在不同的协程中传递。
time.Sleep 的底层也是通过 Timer 实现的。
time.AfterFunc
除了 time.After 和 time.Sleep 以外,还有 AfterFunc 函数。作用类似 After,可以在延迟一段时间后触发某个函数。 下面是输出 1 ,然后延迟 1 秒后输出 2 的例子。
func main() { fmt.Println("1") c := make(chan int, 1) time.AfterFunc(1*time.Second, func() { fmt.Println("2") c <- 1 }) <-c }
定时执行
定时执行又分两种情况,一种是执行 N 次后结束,另一种是程序不终止一直执行。
time.NewTicker
下面是输出 1 之后间每隔 1 秒输出 2,连续输出 5 次后结束的例子。
func main() { fmt.Println("1") count := 0 timeTicker := time.NewTicker(1 * time.Second) for { <-timeTicker.C fmt.Println("2") count++ if count >= 5 { timeTicker.Stop() } } }
time.Tick
下面是每隔 1 秒输出 2,程序不终止一直循环输出的例子。
func main() { t := time.Tick(1 * time.Second) for { <-t fmt.Println("每隔 1 秒输出一次") } }
控制定时器
定时器提供了 Stop 方法和 Reset 方法。
Stop 方法的作用是停止定时器,Reset 方法的作用是改变定时器的间隔时间。
time.Stop
Stop 方法的应用在上面的实例中已经展示了。
time.Reset
下面是一个输出 1 之后每隔 1 秒输出 2 ,输出 3 次 2 后改为每隔 2 秒输出 2 的例子。
func main() { fmt.Println("1") count := 0 timeTicker := time.NewTicker(1 * time.Second) for { <-timeTicker.C fmt.Println("2") count++ if count >= 3 { timeTicker.Reset(2 * time.Second) } } }
源码分析
Go 程序中所有的定时器共同使用一个协程来管理,而不是每个定时器启用一个协程。
time.Timer
以上几个 API 都是基于结构体 Timer 实现。
type Timer struct { C <-chan Time r runtimeTimer }
C 是单次时间间隔的 channel,每次 Timer 到期,会将当前时间发送给 C。
另一个属性是 r,类型是 runtimeTimer,它是内部结构,不会暴漏给开发者。
type runtimeTimer struct { pp uintptr when int64 period int64 f func(interface{}, uintptr) // NOTE: must not be closure arg interface{} seq uintptr nextwhen int64 status uint32 }
time.NewTimer
要理解 runtimeTimer 的作用,要来看创建 Timer 的 NewTimer 函数。
func NewTimer(d Duration) *Timer { c := make(chan Time, 1)// 创建缓冲管道 t := &Timer{ C: c,// 新创建的管道 r: runtimeTimer{ when: when(d),// 触发时间 f: sendTime,// 触发后执行的函数 arg: c,// 触发后 sendTime 函数执行时的参数 }, } startTimer(&t.r)// 启动定时器,将创建的 runtimeTimer 放到系统协程的堆中 return t// 返回定时器 }
time.when
该函数在 sleep.go 中,是一个内部函数,作用是计算出下一次执行的绝对时间。
func when(d Duration) int64 { if d <= 0 { return runtimeNano() } t := runtimeNano() + int64(d) if t < 0 { t = 1<<63 - 1 // math.MaxInt64 } return t }
time.sendTime
将当前时间放入管道,用于触发定时器。
func sendTime(c interface{}, seq uintptr) { select { case c.(chan Time) <- Now(): default: } }
由于创建的管道 C 是缓冲管道,所以不会产生阻塞。sendTime 发送完当前时间后就会退出。
default 空分支的作用是 Ticker 也使用了 sendTime,Ticker 触发时也会向管道发送时间,但无法保证之前的时间已被取走,所以使用 select 搭配 default 保证 sendTime 不会阻塞。
startTimer 在 runtime 包中,作用是把 runtimeTimer 添加到系统协程的数组中,如果系统协程未启动,还会启动系统协程。
time.Stop
stop 简单的调用了 runtime 包中的 stopTimer 方法,系统协程不再监控 Timer,但是 Timer 的管道并不会关闭,因为用户协程还可能读取这个管道。
Timer 如果已经触发,再调用 Stop,会返回 false,表示 Stop 失败。
func (t *Timer) Stop() bool { if t.r.f == nil { panic("time: Stop called on uninitialized Timer") } return stopTimer(&t.r) }
time.Reset
reset 调用了 runtime 包的 modTimer,会把 Timer 删除掉,再修改时间,重新添加到系统协程中。
返回值与 stop 保持一致。
func (t *Ticker) Reset(d Duration) { if t.r.f == nil { panic("time: Reset called on uninitialized Ticker") } modTimer(&t.r, when(d), int64(d), t.r.f, t.r.arg, t.r.seq) }