做后端业务开发经常会用到定时器,今天就分享下定时器的实现吧。定时器实现方式有多种,常见的有最小堆、双向链表、时间轮等,这里主要分享下最小堆实现方式,用的是Golang的heap包。
为啥使用最小堆实现定时器?
最小堆有2个性质:
- 完全二叉树,不懂的小伙伴可以google下
- 每个节点都必须大于/小于叶子节点,根最小的叫最小堆,根最大的叫最大堆
最小堆底层存储是个有序数组。存储定时器的话,就相当于按照执行实现最早的存储在最前面,这样只需要按照顺序遍历定时器,直到有不满足条件的(当前时间 < 执行时间)就退出循环。
具体实现
1、启动调度器
通过启动一个goruntine
来进行定时器调度,这里我启动的是一个秒级定时器,每秒调用时间管理器的RunTimer
方法
func main() {
closeChan := make(chan struct{})
// 启动调度器
timerManager := timer.NewTimerManager()
go scheduler(timerManager)
<-closeChan
}
// 启动调度器
func scheduler(timerManager *timer.Manager) {
ticker := time.NewTicker(1000 * time.Millisecond)
for {
select {
case <-ticker.C:
timerManager.RunTimer()
}
}
}
2、创建时间管理器
时间管理器的数据结构主要有id
、tq
和execTimer
。其中id
是自增的,保证所有定时器的id
不同,tq
是就是最小堆,存储的就是定时器本身,execTimer
存储的是当前正在执行的定时器。
时间管理器主要方法有AddTimer
、RemoveTimer
和RunTimer
。顾名思义就是添加、删除和运行。
AddTimer
需要传入一个TimeOuter
接口,然后创建一个Timer
,然后存储到堆中,然后返回timerId
type TimerOuter interface {
TimeOut()
}
RemoveTimer
方法就是遍历所有Timer
,找到timerId
相同的,然后把timer
的index
传入进行删除
RunTimer
方法先从堆顶取一个定时器,时间不满足直接跳出循环;满足的话就从堆中pop
出来,然后加到execTimer
中,如果间隔时间不为0,把timer
的endTime
设为endTime + interval
,表示间隔多少时间再次执行。最后遍历可执行定时器进行执行即可
package timer
import (
"container/heap"
"time"
)
type Manager struct {
id uint32 // 自增的timerId
tq TimerQueue // 定时器
execTimer []interface{} // 将要执行的timer
}
func NewTimerManager() *Manager {
return &Manager{
tq: make(TimerQueue, 0, 1024),
}
}
// 添加定时器
func (this *Manager) AddTimer(timerOuter TimerOuter, endTime uint32, interval uint32) uint32 {
// id自增
this.id++
timer := &Timer{
id: this.id,
TimerOuter: timerOuter,
endTime: endTime,
interval: interval,
}
// 添加到堆中
heap.Push(&this.tq, timer)
// 将定时器id返回,调用层保存定时器id,通过定时器id来删除定时器
return this.id
}
// 删除定时器
func (this *Manager) RemoveTimer(timerId uint32) {
for _, timer := range this.tq {
if timer.id == timerId {
heap.Remove(&this.tq, timer.index)
return
}
}
}
// 执行定时器
func (this *Manager) RunTimer() {
// 没有需要执行的任务
if this.tq.Len() <= 0 {
return
}
for this.tq.Len() > 0 {
// 从堆顶取一个
tmp := this.tq[0]
// 时间未到
if uint32(time.Now().Unix()) < tmp.endTime {
break
}
timer := heap.Pop(&this.tq).(*Timer)
this.execTimer = append(this.execTimer, timer)
// 下次可执行
if timer.interval > 0 {
timer.endTime += timer.interval
heap.Push(&this.tq, timer)
}
}
// 执行定时器
if len(this.execTimer) > 0 {
for _, timer := range this.execTimer {
timer.(TimerOuter).TimeOut()
}
}
// 清空
this.execTimer = this.execTimer[:0]
}
3、创建Timer结构体
index
需要重点说下,存储这个是为了方便删除timer的时候用的
type Timer struct {
TimerOuter // 执行方法
id uint32 // 定时器id
endTime uint32 // 执行时间
interval uint32 // 间隔时间
index int // 在堆数组中的索引
}
4、创建TimerQueue
Golang使用最小堆需要实现heap
的interface
,又因为包含了sort.Interface
,所有还需要实现sort
接口。
type Interface interface {
sort.Interface
Push(x interface{}) // add x as element Len()
Pop() interface{} // remove and return element Len() - 1.
}
TimerQueue
package timer
type TimerQueue []*Timer
func (pq TimerQueue) Len() int { return len(pq) }
func (pq TimerQueue) Less(i, j int) bool {
return pq[i].endTime < pq[j].endTime
}
func (pq TimerQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *TimerQueue) Push(x interface{}) {
n := len(*pq)
item := x.(*Timer)
item.index = n
*pq = append(*pq, item)
}
func (pq *TimerQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
item.index = -1 // for safety
*pq = old[0: n-1]
return item
}
测试执行
首先开启调度器,然后添加a,b,c三个定时器,过20秒删除定时器b
func main() {
closeChan := make(chan struct{})
// 开启调度器
timerManager := timer.NewTimerManager()
go scheduler(timerManager)
time.Sleep(time.Second * 1)
// 添加定时器
now := uint32(time.Now().Unix())
timerManager.AddTimer(&timer.TimerCallback{CallBack: A}, now, 0)
timerId := timerManager.AddTimer(&timer.TimerCallback{CallBack: B}, now, 5)
timerManager.AddTimer(&timer.TimerCallback{CallBack: C}, now, 10)
// 删除定时器
time.Sleep(time.Second * 20)
timerManager.RemoveTimer(timerId)
<-closeChan
}
func A() {
now := uint32(time.Now().Unix())
fmt.Printf("%v => aaa\n", now)
}
func B() {
now := uint32(time.Now().Unix())
fmt.Printf("%v => bbb\n", now)
}
func C() {
now := uint32(time.Now().Unix())
fmt.Printf("%v => ccc\n", now)
}
细心的同学可能发现了AddTimer
方法传入的参数是TimerCallBack
,这个是用于将定时器执行方法转成TimerOuter
接口
package timer
type TimerCallback struct {
CallBack func()
}
func (this *TimerCallback) TimeOut() {
if this.CallBack != nil {
this.CallBack()
}
}
执行结果
GitHub: github.com/webbc/timer