在上一篇文章中, 我们对标准库中的定时器相关功能进行了熟悉, 并且简单介绍了一种高性能的层级时间轮. 在本篇文章中, 将开始讲解如何参考 [Kafka Purgatory](https://www.confluent.io/blog/apache-kafka-purgatory-hierarchical-timing-wheels/) 去实现一个简单的层级时间轮.
整体结构
一个层级时间轮的实现中, 主要包括三个部分: 优先级队列, 延时队列以及时间轮自身. 它们三者的关系如下:

其中:
一个层级时间轮中有多层的时间轮, 每个轮盘上有多个插槽, 每个插槽都包含有相应时间范围内的定时器
一个层级时间轮中有一个延时队列, 如果一个插槽中包含有定时器, 则该插槽的触发时间会被添加到延时队列, 并在该时间到期时从队列中弹出
延时队列会包含有多个触发时间, 所有的触发时间都通过优先级队列进行排序
当一个定时任务被放置到层级时间轮之中时, 流程如下:
按照任务的触发时间添加到某一层的时间轮的插槽中, 如果时间轮不存在则创建.
如果插槽的触发时间发生了变化, 则把对应的插槽添加到延时队列
延时队列在等待对应的触发时间之后, 将改元素弹出
层级时间轮收到插槽弹出的时间之后, 遍历该插槽中的定时任务, 如果到达触发时间则触发改任务, 否则重新插入到层级时间轮
优先级队列
优先级队列是基于最小堆的实现, 最小堆是一种经过排序的二叉树结构, 它的特点是其中任何一个非终端节点的数据值均不会大于其左子节点和右子节点的数据值. 这样可以很容易的证明, 其中最小的值一定在根节点元素.
基于最小堆我们可以很方便的实现优先级队列, 只需要将优先级作为节点的数据值即可.

在标准库的 container/heap 包中, 实现了多个用于辅助最小堆实现的函数, 利用它们, 一个非常简易的优先级队列实现如下:
// Element for priority queue elementtype Element struct {// Value for elementValue interface{}// priority of the element, make it private to avoid change from elementpriority int64// index of the element in the sliceindex int// pq is the refer of priority queuepq *priorityQueue}// ...// PriorityQueue for priority queue traittype PriorityQueue interface {// Add element to the PriorityQueue, it will return the element witch been addedAdd(v interface{}, priority int64) *Element// Peek return the lowest priority elementPeek() *Element// Pop return the lowest priority element and remove itPop() *Element// Remove will remove the element from the priority queueRemove(v *Element) error// Update the element in the priority queue with the new priorityUpdate(v *Element, priority int64) error// Size return the element size of queueSize() int}// elems is an slice of Elements, it implement the heap.Interfacetype elems = []*Element// heapi implement the heap.Interfacetype heapi struct {pq *priorityQueue}// ...// Push the value at the end of slice, implement heap.Pushfunc (h *heapi) Push(x interface{}) {h.pq.e = append(h.pq.e, x.(*Element))}// Pop the value at the last position of slice, implement heap.Popfunc (h *heapi) Pop() interface{} {old := h.pq.en := len(old)if n == 0 {return nil}// set element to nil for GCx := old[n-1]old[n-1] = nilh.pq.e = old[0 : n-1]return x}// priorityQueue is a implement by min heap, the 0th element is the lowest valuetype priorityQueue struct {e elemsh *heapi}// NewPriorityQueue construct a PriorityQueuefunc NewPriorityQueue(size int) PriorityQueue {pq := &priorityQueue{e: make(elems, 0, size),}pq.h = &heapi{pq: pq,}return pq}// Add element to the PriorityQueue, it will return the element witch been addedfunc (pq *priorityQueue) Add(x interface{}, priority int64) *Element {e := &Element{Value: x,priority: priority,index: len(pq.e),pq: pq,}heap.Push(pq.h, e)return e}// Pop return the lowest priority element and remove itfunc (pq *priorityQueue) Pop() *Element {if len(pq.e) == 0 {return nil}x := heap.Pop(pq.h)e := x.(*Element)e.index = -1e.pq = nilreturn e}// Remove will remove the element from the priority queuefunc (pq *priorityQueue) Remove(e *Element) error {if e.pq != pq {return fmt.Errorf("PriorityQueue.Remove: QueueMatchFailed: Element[%v], Queue[%v]", e.pq, pq)}if e.index < 0 || e.index >= len(pq.e) {return fmt.Errorf("PriorityQueue.Remove: OutOfIndex: Index[%v], Len[%v]", e.index, len(pq.e))}if e.priority != pq.e[e.index].priority {return fmt.Errorf("PriorityQueue.Remove: PriorityMatchFailed: Element[%v], Queue[%v]", e.priority, pq.e[e.index].priority)}heap.Remove(pq.h, e.index)e.index = -1e.pq = nilreturn nil}
将所有元素放置在一个切片中, 模拟一个二叉树的结构
将元素添加到切片之后, 调用 heap.Push 来完成最小堆结构的调整
将元素从切片中弹出或者移除元素之后, 调用 heap.Pop 或者 heap.Remove 来完成最小堆结构的调整
延时队列
在有一个优先级队列的实现之后, 可以在这个结构之上快速的实现一个延时队列, 延时队列的接口定义如下:
// DelayQueue is an blocking queue of *Delay* elements, the element// can only been taken when its delay has expired. The head of the queue// is the element whose delay expired most recent in the queue.type DelayQueue interface {// Offer insert the element into the current DelayQueue,// if the expiration is blow the current min expiration, the item will// been fired first.Offer(elem interface{}, expireation int64)// Poll starts an infinite loop, it will continually waits for an element to// been fired, and send the element to the output Chan.Poll(ctx context.Context)// Chan return the output chan, when the element is fired the element// will send to the channel.Chan() <-chan interface{}// Size return the element count in the queueSize() int}
其中最重要的是 Offer 以及 Poll 两个函数, 以及 Chan 函数返回的通道. 我们可以通过 Offer 将延时元素放入队列, 并且通过 Poll 函数来启动延时队列, 最后从 Chan 函数返回的通道中读取触发的元素.
接下来我们看看 Offer 函数的实现:
// Offer implement the DelayQueue.Offerfunc (q *delayQueue) Offer(element interface{}, expireation int64) {_push := func() (*Element, int) {q.mu.Lock()defer q.mu.Unlock()e := q.pq.Add(element, expireation)return e, e.Index()}_, index := _push()// there is no concurrent protection, EX:// 1. goroutine1 add element with expireation 100// 2. goroutine2 add element with expireation 50// 3. the both goroutine get the element index 0// 4. goroutine2 cas the sleeping state to 0, and send the wakeup signal// 5. pool wakeup and update the fired point, cas the sleeping state to 1// 6. goroutine1 cas the sleeping state to 0, and send the wakeup signal// 7. pool wakeup and update the fired point, cas the sleeping state to 1// because the pool always update the fired point to the min expireation, so there is no problem(always update to 50)if index == 0 {// the element is the first element(with the earliest expireation), we// need week up the Pool loop to update the fired pointif atomic.CompareAndSwapInt32(&q.sleeping, 1, 0) {// if we change the sleeping state from sleep to weekup success, send the signal to wakepupCq.wakeupC <- struct{}{}}}}
首先将元素添加到优先级队列中(根据触发时间进行排序), 并且如果是队列中的最优先的元素(具有最小的触发时间), 则通过一个内部的通道唤醒等待中的 goroutine.
接下来看 Poll 函数的实现(具体内容在 pollImpl 函数中):
// Poll implement the DelayQueue.Poolfunc (q *delayQueue) Poll(ctx context.Context) {defer func() {// reset the state to wakeupatomic.StoreInt32(&q.sleeping, 0)}()// an infinite loop// 1. wakeup at the min expiration// 2. send to the Cfor poll(ctx, q) {}}var (poll func(ctx context.Context, q *delayQueue) bool)// the inner implement of poll, split from Poll for test// return true if been wakeup or fired, false to shutdown the loopfunc pollImpl(ctx context.Context, q *delayQueue) bool {n := q.T.Now()q.mu.Lock()item := q.pq.Peek()if item == nil || item.Priority() > n {// No item left, change the sleeping state to 1atomic.StoreInt32(&q.sleeping, 1)}q.mu.Unlock()// we have got the min expiration item, it maybe nil for empty pqif item == nil {// wait for wakeup (new item Offer into the queue)select {case <-ctx.Done():return falsecase <-q.wakeupC:return true}}// have item, wait for the fired pointdelta := item.Priority() - nif delta <= 0 {// the item need fired, send the value to the output channelselect {case q.C <- item.Value:// the element is firedq.mu.Lock()_ = q.pq.Remove(item)q.mu.Unlock()return truecase <-ctx.Done():return false}}// the item is pending, wait for fired or new min element addselect {case <-q.wakeupC:return truecase <-time.After(time.Duration(delta) * time.Millisecond):// we doesn't fired the item at there, go to next loop and the item will been fired because delta <= 0if atomic.SwapInt32(&q.sleeping, 0) == 0 {// if the old state is wakeup, the maybe an signal in wakeupC,// so we drain it the unblock the callerselect {case <-q.wakeupC:default:}}return truecase <-ctx.Done():return false}}
整个 Poll 函数是一个无限循环, 只有当 ctx 被取消时候才会退出. 在循环中主要有以下几部分的逻辑:
从优先级队列中取出队列头部的元素, 如果队列中元素为空则睡眠在内部通道上, 等待被 Offer 函数唤醒
如果该元素已经到达触发时间, 则发送到返回通道中, 并且从优先级队列中移除该元素, 表示该元素已经触发
如果该元素还未到达触发时间, 则通过 time.After 等待对应的超时时间, 然后进入下一轮循环; 或者被 Offer 中添加的新元素唤醒, 进入下一轮循环.
总结结构
在本篇文章中我们从层级时间轮的整体结构, 以及优先级队列和延时队列的简单实现中进行了一番探索. 在后续文章中会对层级时间轮自身如何实现进行进一步梳理.
参考资料
[Hashed and Hierarchical Timing Wheels, 层级时间轮](http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf)
[Kafka Purgatory](https://www.confluent.io/blog/apache-kafka-purgatory-hierarchical-timing-wheels/)
[DelayQueue](https://github.com/lsytj0413/ena/tree/master/delayqueue)




