Golang定时器实现

647 阅读2分钟

做后端业务开发经常会用到定时器,今天就分享下定时器的实现吧。定时器实现方式有多种,常见的有最小堆、双向链表、时间轮等,这里主要分享下最小堆实现方式,用的是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、创建时间管理器

时间管理器的数据结构主要有idtqexecTimer。其中id是自增的,保证所有定时器的id不同,tq是就是最小堆,存储的就是定时器本身,execTimer存储的是当前正在执行的定时器。

时间管理器主要方法有AddTimerRemoveTimerRunTimer。顾名思义就是添加、删除和运行。

AddTimer需要传入一个TimeOuter接口,然后创建一个Timer,然后存储到堆中,然后返回timerId

type TimerOuter interface {
	TimeOut()
}

RemoveTimer方法就是遍历所有Timer,找到timerId相同的,然后把timerindex传入进行删除

RunTimer 方法先从堆顶取一个定时器,时间不满足直接跳出循环;满足的话就从堆中pop出来,然后加到execTimer中,如果间隔时间不为0,把timerendTime设为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使用最小堆需要实现heapinterface,又因为包含了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