Golang中使用heap编写一个简单高效的定时器模块

Posted Golang服务端开发之道

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang中使用heap编写一个简单高效的定时器模块相关的知识,希望对你有一定的参考价值。

定时器模块在服务端开发中非常重要,一个高性能的定时器模块能够大幅度提升引擎的运行效率。使用Golang和heap实现一个通用的定时器模块,代码来自:https://github.com/xiaonanln/goTimer

也可以查看文档:http://godoc.org/github.com/xiaonanln/goTimer,下面是完整的代码,并进行适当的注释和分析。

从性能测试结果来看,基于heap的定时器模块在效率上并不会比时间轮(TimeWheel)的实现慢多少,但是逻辑上要简单很多。

源代码加注释:

package timer

import (
	"container/heap" // Golang提供的heap库
	"fmt"
	"os"
	"runtime/debug"
	"sync"
	"time"
)

const (
	MIN_TIMER_INTERVAL = 1 * time.Millisecond // 循环定时器的最小时间间隔
)

var (
	nextAddSeq uint = 1 // 用于为每个定时器对象生成一个唯一的递增的序号
)

// 定时器对象
type Timer struct { 
	fireTime  time.Time // 触发时间
	interval  time.Duration // 时间间隔(用于循环定时器)
	callback  CallbackFunc // 回调函数
	repeat    bool // 是否循环
	cancelled bool // 是否已经取消
	addseq    uint // 序号
}

// 取消一个定时器,这个定时器将不会被触发
func (t *Timer) Cancel() {
	t.cancelled = true
}

// 判断定时器是否已经取消
func (t *Timer) IsActive() bool {
	return !t.cancelled
}

// 使用一个heap管理所有的定时器
type _TimerHeap struct {
	timers []*Timer
}

// Golang要求heap必须实现下面这些函数,这些函数的含义都是不言自明的

func (h *_TimerHeap) Len() int {
	return len(h.timers)
}

// 使用触发时间和需要对定时器进行比较
func (h *_TimerHeap) Less(i, j int) bool {
	//log.Println(h.timers[i].fireTime, h.timers[j].fireTime)
	t1, t2 := h.timers[i].fireTime, h.timers[j].fireTime
	if t1.Before(t2) {
		return true
	}

	if t1.After(t2) {
		return false
	}
	// t1 == t2, making sure Timer with same deadline is fired according to their add order
	return h.timers[i].addseq < h.timers[j].addseq
}

func (h *_TimerHeap) Swap(i, j int) {
	var tmp *Timer
	tmp = h.timers[i]
	h.timers[i] = h.timers[j]
	h.timers[j] = tmp
}

func (h *_TimerHeap) Push(x interface{}) {
	h.timers = append(h.timers, x.(*Timer))
}

func (h *_TimerHeap) Pop() (ret interface{}) {
	l := len(h.timers)
	h.timers, ret = h.timers[:l-1], h.timers[l-1]
	return
}

// 定时器回调函数的类型定义
type CallbackFunc func()

var (
	timerHeap     _TimerHeap // 定时器heap对象
	timerHeapLock sync.Mutex // 一个全局的锁
)

func init() {
	heap.Init(&timerHeap) // 初始化定时器heap
}

// 设置一个一次性的回调,这个回调将在d时间后触发,并调用callback函数
func AddCallback(d time.Duration, callback CallbackFunc) *Timer {
	t := &Timer{
		fireTime: time.Now().Add(d),
		interval: d,
		callback: callback,
		repeat:   false,
	}
	timerHeapLock.Lock() // 使用锁规避竞争条件
	t.addseq = nextAddSeq 
	nextAddSeq += 1

	heap.Push(&timerHeap, t)
	timerHeapLock.Unlock()
	return t
}

// 设置一个定时触发的回调,这个回调将在d时间后第一次触发,以后每隔d时间重复触发,并调用callback函数
func AddTimer(d time.Duration, callback CallbackFunc) *Timer {
	if d < MIN_TIMER_INTERVAL {
		d = MIN_TIMER_INTERVAL
	}

	t := &Timer{
		fireTime: time.Now().Add(d),
		interval: d,
		callback: callback,
		repeat:   true, // 设置为循环定时器
	}
	timerHeapLock.Lock()
	t.addseq = nextAddSeq // set addseq when locked
	nextAddSeq += 1

	heap.Push(&timerHeap, t)
	timerHeapLock.Unlock()
	return t
}

// 对定时器模块进行一次Tick
// 
// 一般上层模块需要在一个主线程的goroutine里按一定的时间间隔不停的调用Tick函数,从而确保timer能够按时触发,并且
// 所有Timer的回调函数也在这个goroutine里运行。
func Tick() {
	now := time.Now()
	timerHeapLock.Lock()

	for {
		if timerHeap.Len() <= 0 { // 没有任何定时器,立刻返回
			break
		}

		nextFireTime := timerHeap.timers[0].fireTime
		if nextFireTime.After(now) { // 没有到时间的定时器,返回
			break
		}

		t := heap.Pop(&timerHeap).(*Timer)

		if t.cancelled { // 忽略已经取消的定时器
			continue
		}

		if !t.repeat {
			t.cancelled = true
		}
<strong>		// 必须先解锁,然后再调用定时器的回调函数,否则可能导致死锁!!!</strong>
		timerHeapLock.Unlock() 
		runCallback(t.callback) // 运行回调函数并捕获panic
		timerHeapLock.Lock()

		if t.repeat { // 如果是循环timer就把Timer重新放回heap中
			// add Timer back to heap
			t.fireTime = t.fireTime.Add(t.interval)
			if !t.fireTime.After(now) {
				t.fireTime = now.Add(t.interval)
			}
			t.addseq = nextAddSeq
			nextAddSeq += 1
			heap.Push(&timerHeap, t)
		}
	}
	timerHeapLock.Unlock()
}

// 创建一个goroutine对定时器模块进行定时的Tick
func StartTicks(tickInterval time.Duration) {
	go selfTickRoutine(tickInterval)
}

func selfTickRoutine(tickInterval time.Duration) {
	for {
		time.Sleep(tickInterval)
		Tick()
	}
}

// 运行定时器的回调函数,并捕获panic,将panic转化为错误输出
func runCallback(callback CallbackFunc) {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Fprintf(os.Stderr, "Callback %v paniced: %v\n", callback, err)
			debug.PrintStack()
		}
	}()
	callback()
}

  

以上是关于Golang中使用heap编写一个简单高效的定时器模块的主要内容,如果未能解决你的问题,请参考以下文章

golang中container/heap包源码分析

golang的定时器简单使用

Go语言实现高并发日志监控系统

golang cron定时任务简单实现

Golang定时任务简单实现

Golang的pprof的使用心得(CPU,Heap)