定时器是一种通过设置一项任务,在未来的某个时刻执行该任务的机制。

定时器的种类通常只有两种,一种是只执行一次的延时模式,一种是每隔一段时间执行一次的间隔模式。

在现代编程语言中,定时器几乎是标配。除了设置定时器外,还需要有提供定时器的方法。

比如在 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)
}复制代码