教你轻松理解Go Ticker的用法和实现原理
前言
定时器在Go语言应用中使用非常广泛,Go语言的标准库里提供两种类型的计时器,一种是一次性的定时器Timer,另外一种是周期性的定时器Ticker。本文主要来看一下Ticker的用法和实现原理。
Ticker
Ticker是周期性定时器,即周期性的触发一个事件,它会以一个间隔(interval)往channel发送一个事件(当前时间),而channel的接收者可以以固定的时间间隔从channel中读取事件。通过Ticker本身提供的管道将事件传递出去。
应用示例
package main import ( "fmt" "time" ) func main() { ticker := time.NewTicker(time.Second * 1) //创建一个周期性定时器 i := 1 for { fmt.Println(i, "====>", <-ticker.C) if i == 5 { ticker.Stop() //停止定时器 break } i++ } }
输出结果:
1 ====> 2022-08-24 15:58:38.971837 +0800 CST m=+1.001366085 2 ====> 2022-08-24 15:58:39.971154 +0800 CST m=+2.000695418 3 ====> 2022-08-24 15:58:40.971633 +0800 CST m=+3.001185460 4 ====> 2022-08-24 15:58:41.97109 +0800 CST m=+4.000654126 5 ====> 2022-08-24 15:58:42.971594 +0800 CST m=+5.001169210
创建定时器
使用NewTicker()方法就可以创建一个周期性定时器,函数如下:
func NewTicker(d Duration) *Ticker
其中参数d即为定时器时间触发的周期,为一个时间段。
停止定时器
使用定时器对外暴露的 Stop 方法就可以停掉一个周期性定时器, 函数如下:
func (t *Ticker) Stop()
该方***停止计时,停止后不会向定时器的管道中写入事件,但管道并不会被关闭。管道在使用完成后,生命周期结束后会自动释放。
实现原理
数据结构
Ticker的数据结构与Timer完全一致:
通过src/time.sleep.go:Ticker定义了Timer数据结构:
type Ticker struct { C <-chan Time
r runtimeTimer
}
它提供了一个channel,在定时时间到达之前,没有数据写入Ticker.C会一直阻塞,直到时间到达,向channel写入系统时间,阻塞解除,可以从中读取数据,这就是一个事件。
我们可以理解为Ticker.C即面向Ticker用户的,Ticker.r是面向底层的定时器实现。
runtimeTimer
runtimeTimer与Timer一样,任务的载体,用于监控定时任务,每创建一个Timer就创建一个runtimeTimer变量,然后把它交给系统进行监控,我们通过设置runtimeTimer过期后的行为来达到定时的目的。
源码包src/time/sleep.go:runtimeTimer定义了其数据结构:
type runtimeTimer struct { tb uintptr // 存储当前定时器的数组地址 i int // 存储当前定时器的数组下标 when int64 // 当前定时器触发时间 period int64 // 当前定时器周期触发间隔 f func(interface{}, uintptr) // 定时器触发时执行的函数 arg interface{} // 定时器触发时执行函数传递的参数一 seq uintptr // 定时器触发时执行函数传递的参数二(该参数只在网络收发场景下使用) }
创建Ticker
func NewTicker(d Duration) *Ticker { if d <= 0 { panic(errors.New("non-positive interval for NewTicker")) } // Give the channel a 1-element time buffer. // If the client falls behind while reading, we drop ticks // on the floor until the client catches up. c := make(chan Time, 1) t := &Ticker{ C: c, r: runtimeTimer{ when: when(d), period: int64(d), // Ticker跟Timer的重要区就是提供了period这个参数,据此决定timer是一次性的,还是周期性的 f: sendTime, arg: c, }, } startTimer(&t.r) return t }
NewTicker()构造了一个Ticker,然后把Ticker.r通过startTimer()交给系统协程维护。
其中period为事件触发的周期。
停止Ticker
停止Ticker,只是把Ticker从系统协程中移除。
func (t *Ticker) Stop() { stopTimer(&t.r) }
stopTicker()即通知系统协程把该Ticker移除,即不再监控。系统协程只是移除Ticker并不会关闭管道,以避免用户协程读取错误。
注意:
Ticker在使用完后务必要释放,否则会产生资源泄露,进而会持续消耗CPU资源,最后会把CPU耗尽。
ticker := time.NewTicker(1 * time.Second) defer ticker.Stop()
Ticker 与 Timer 区别
- Ticker —— 重复性执行任务非常有用呢
- Timer —— 用于执行一次性任务
小结
Ticker相关内容总结如下:
- 使用time.NewTicker()来创建一个定时器;
- 使用Stop()来停止一个定时器;
- 定时器使用完毕要释放,否则会产生资源泄露;