设计思路
在Linux的下实现定时器主要有如下方式
基于链表实现定时器
基于排序链表实现定时器
基于最小堆实现定时器
基于时间轮实现定时器
这在当中基于时间轮方式实现的定时器时间复杂度最小,效率最高,然而可以我们通过优先队列实现时间轮定时器。
优先队列的实现可以使用最大堆和最小堆,因此在队列中所有的数据都可以定义排序规则自动排序。直接我们通过队列中pop
函数电子杂志数据,就是我们按照自定义排序规则想要的数据。
在Golang
中实现一个优先队列异常简单,在container/head
包中已经帮我们封装了,实现的细节,我们只需要实现特定的接口就可以。
下面是官方提供的例子
// This example demonstrates a priority queue built using the heap interface.// An Item is something we manage in a priority queue.type Item struct { value string // The value of the item; arbitrary. priority int // The priority of the item in the queue. // The index is needed by update and is maintained by the heap.Interface methods. index int // The index of the item in the heap.}// A PriorityQueue implements heap.Interface and holds Items.type PriorityQueue []*Itemfunc (pq PriorityQueue) Len() int { return len(pq) }func (pq PriorityQueue) Less(i, j int) bool { // We want Pop to give us the highest, not lowest, priority so we use greater than here. return pq[i].priority > pq[j].priority }func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] pq[i].index = i pq[j].index = j }func (pq *PriorityQueue) Push(x interface{}) { n := len(*pq) item := x.(*Item) item.index = n *pq = append(*pq, item) }func (pq *PriorityQueue) Pop() interface{} { old := *pq n := len(old) item := old[n-1] item.index = -1 // for safety *pq = old[0 : n-1] return item }
因为优先队列底层数据结构是由二叉树构建的,所以我们可以通过数组来保存二叉树上的每一个节点。
改数组需要实现Go
预先定义的接口Len
,Less
,Swap
,Push
,Pop
和update
。
Len
接口定义返回队列长度Swap
接口定义队列数据优先级,比较规则Push
接口定义推送数据到队列中操作Pop
接口定义返回队列中顶层数据,并且将改数据删除update
接口定义更新队列中数据信息
接下来我们分析https://github.com/leesper/tao开源的代码中TimeingWheel中的实现细节。
一,设计细节
1.结构细节
1.1定时任务结构
type timerType struct { id int64 expiration time.Time interval time.Duration timeout *OnTimeOut index int // for container/heap}type OnTimeOut struct { Callback func(time.Time, WriteCloser) Ctx context.Context}
timerType结构是定时任务抽象结构
id
定时任务的唯一ID,可以这个ID查找在队列中的定时任务expiration
定时任务的到期时间点,当到这个时间点后,触发定时任务的执行,在优先队列中也是通过这个字段来排序interval
定时任务的触发频率,每隔间隔时间段触发一次timeout
这个结构中保存定时超时任务,这个任务函数参数必须符合相应的接口类型index
保存在队列中的任务所在的下标
1.2时间轮结构
type TimingWheel struct { timeOutChan chan *OnTimeOut timers timerHeapType ticker *time.Ticker wg *sync.WaitGroup addChan chan *timerType // add timer in loop cancelChan chan int64 // cancel timer in loop sizeChan chan int // get size in loop ctx context.Context cancel context.CancelFunc }
timeOutChan
定义一个带缓存的陈来保存,已经触发的定时任务timers
的英文[]*timerType
类型的片,保存所有定时任务ticker
当每一个股票到来时,时间轮都会检查队列中的头元素是否到达超时时间wg
用于并发控制addChan
通过带缓存的陈来向队列中添加任务cancelChan
定时器停止的陈sizeChan
返回队列中任务的数量的陈ctx
状语从句:cancel
用户并发控制
2.关键函数实现
2.1 TimingWheel的主循环函数
func (tw *TimingWheel) start() { for { select { case timerID := <-tw.cancelChan: index := tw.timers.getIndexByID(timerID) if index >= 0 { heap.Remove(&tw.timers, index) } case tw.sizeChan <- tw.timers.Len(): case <-tw.ctx.Done(): tw.ticker.Stop() return case timer := <-tw.addChan: heap.Push(&tw.timers, timer) case <-tw.ticker.C: timers := tw.getExpired() for _, t := range timers { tw.TimeOutChannel() <- t.timeout } tw.update(timers) } } }
的首先start
函数,创建³³当一个TimeingWheel
时,一个通过goroutine
来执行start
,在启动中用于循环和选择来监控不同的信道的状态
<-tw.cancelChan
返回要取消的定时任务的ID,并且在队列中删除tw.sizeChan <-
将定时任务的个数放入这个无缓存的通道中<-tw.ctx.Done()
当父上下文执行取消时,该通道就会有数值,表示该TimeingWheel
要停止<-tw.addChan
通过带缓存的addChan来向队列中添加任务<-tw.ticker.C
ticker定时,当每一个ticker到来时,time包就会向该渠道中放入当前时间,当每一个Ticker到来时,TimeingWheel都需要检查队列中到到期的任务(tw.getExpired()
),通过范围来放入TimeOutChannel
频道中,最后在更新队列。
2.2 TimingWheel的寻找超时任务函数
func (tw *TimingWheel) getExpired() []*timerType { expired := make([]*timerType, 0) for tw.timers.Len() > 0 { timer := heap.Pop(&tw.timers).(*timerType) elapsed := time.Since(timer.expiration).Seconds() if elapsed > 1.0 { dylog.Warn(0, "timing_wheel", nil, "elapsed %d", elapsed) } if elapsed > 0.0 { expired = append(expired, timer) continue } else { heap.Push(&tw.timers, timer) break } } return expired }
通过对循环从队列中取数据,直到该队列为空或者是遇见第一个当前时间比任务开始时间大的任务,append
到expired
中,优先因为队列中的英文根据expiration
来排序的,
所以当取到第一个定时任务未到的任务时,表示该定时任务以后的任务都未到时间。
2.3 TimingWheel的更新队列函数
func (tw *TimingWheel) update(timers []*timerType) { if timers != nil { for _, t := range timers { if t.isRepeat() { // repeatable timer task t.expiration = t.expiration.Add(t.interval) // if task time out for at least 10 seconds, the expiration time needs // to be updated in case this task executes every time timer wakes up. if time.Since(t.expiration).Seconds() >= 10.0 { t.expiration = time.Now() } heap.Push(&tw.timers, t) } } } }
当getExpired
函数取出队列中要执行的任务时,当有的定时任务需要不断执行,所以就需要判断是否该定时任务需要重新放回优先队列中。isRepeat
是通过判断任务中interval
是否大于0判断,
如果大于0则,表示永久就生效。
3. TimeingWheel的用法
防止外部滥用,阻塞定时器协程,框架又一次封装了计时器这个包,名为timer_wapper
这个包,它提供了两种调用方式。
3.1第一种普通的调用定时任务
func (t *TimerWrapper) AddTimer(when time.Time, interv time.Duration, cb TimerCallback) int64{ return t.TimingWheel.AddTimer( when, interv, serverbase.NewOnTimeOut(t.ctx, func(t time.Time, c serverbase.WriteCloser) { cb() })) }
AddTimer添加定时器任务,任务在定时器协程执行
当为执行时间
INTERV为执行周期,INTERV = 0只执行一次
CB为回调函数
3.2第二种通过任务池调用定时任务
func (t *TimerWrapper) AddTimerInPool(when time.Time, interv time.Duration, cb TimerCallback) int64 { return t.TimingWheel.AddTimer( when, interv, serverbase.NewOnTimeOut(t.ctx, func(t time.Time, c serverbase.WriteCloser) { workpool.WorkerPoolInstance().Put(cb) })) }
参数和上面的参数一样,只是在第三个参数中使用了任务池,将定时任务放入了任务池中。任务接通本节能的本身执行就是一个put
操作。
至于穿上以后,就是那workers
这个包管理的了。在worker
包中,也就是维护了一个任务池,任务池中的任务会有序的执行,方便管理。
作者:wiseAaron
链接:https://www.jianshu.com/p/17cd7d2b4800
共同学习,写下你的评论
评论加载中...
作者其他优质文章