【前言】定时器大家应该都使用过? 那么您有想过定时器要怎么设计吗? 本文为大家介绍了 时间轮 定时器的设计思想和简单实现,希望能对大家有所帮助。

时间轮算法(Timing-Wheel)很早出现在linux kernel 2.6中。因效率非常高,很多应用框架都实现了这个算法。还有些定时器使用最小堆实现,但总体来说,时间轮算法在插入性能上更高。

前面分析libco的时候,也讲到其实现了一个时间轮定时器,不过这个定时器只有一个轮,且长度是60000,仅仅实现了60秒的定时器范围,这个不免有些残缺。

这一篇想介绍一个完整的定时器实现,使用5个轮子,一共512个结点,如果精度是1毫秒,可以定时长达2^32-1毫秒这么长,粗略算起来接近50天。

时间轮分成多个层级,每一层是一个圈,和时钟类似,和水表更像,如下面图:



iOS 毫秒转时分 毫秒转时间日期_链表

当个位的指针转完一圈到达0这个刻度之后,十位的指针转1格;当十位的转完一圈,百位的转1格,以此类推。这样就能表示很长的度数。

时间轮能表达的时间长度,和圈的数量以及每一圈的长度成正比。假设有5圈,每个圈60个刻度,每个刻度表示1毫秒,那么这个时间轮可以表示这么长:

60 x 60 x 60 x 60 x 60 = 777,600,000‬(ms) ~ 216小时

现在用程序表示这个指针可能这样的:

int ptr[5];

假如有一个update函数驱动时间轮运转起来,每调一次跳一格,那这个算法可能是这样的:

  • ptr[0]加1,如果ptr[0]等于60,则使ptr[0]重置为0,可以写为:ptr[0] = (ptr[0] + 1) % 60
  • 如果ptr[0]等于0的时候,说明转了一圈,此时ptr[1]要加1:ptr[1] = (ptr[1] + 1) % 60
  • 就这样一直转到第5圈,然后又重新开始。

这样处理有点麻烦,而且还需要一个数组来表示。其实可以用一个uint32变量来划分,比如:

| 6bit | 6bit | 6bit | 6bit |  8bit  |
 111111 111111 111111 111111 11111111

也分5个圈,第1个占8位即256个槽位,后面4个分别占6位即64个槽位。这个变量只需不断自增就行,比如前面8位满了会变成0,后面自动进1。这样就可以定义一些宏:

// 第1个轮占的位数
#define TVR_BITS 8
// 第1个轮的长度
#define TVR_SIZE (1 <// 第n个轮占的位数#define TVN_BITS 6// 第n个轮的长度#define TVN_SIZE (1 <// 掩码:取模或整除用#define TVR_MASK (TVR_SIZE - 1)#define TVN_MASK (TVN_SIZE - 1)

想取某一个圈的当前指针位置是:

// 第1个圈的当前指针位置
#define FIRST_INDEX(v) ((v) & TVR_MASK)
// 后面第n个圈的当前指针位置
#define NTH_INDEX(v, n) (((v) >> (TVR_BITS + (n) * TVN_BITS)) & TVN_MASK)

核心的数组结构是这样的:

// 第1个轮
typedef struct tvroot {
    clinknode_t vec[TVR_SIZE];
} tvroot_t;
// 后面几个轮
typedef struct tvnum {
    clinknode_t vec[TVN_SIZE];
} tvnum_t;

// 时间轮定时器
typedef struct timerwheel {
    tvroot_t tvroot;               // 第1个轮
    tvnum_t tv[4];                 // 后面4个轮
    uint64_t lasttime;             // 上一次的时间毫秒
    uint32_t currtick;             // 当前的tick
    uint16_t interval;             // 每个时间点的毫秒间隔
    uint16_t remainder;            // 剩余的毫秒
} timerwheel_t;

这里要重点讲一下clinknode_t,这是每一个圈的槽位的数据,它其实是一个双向循环链表:



iOS 毫秒转时分 毫秒转时间日期_#define_02

新结点可以往head的前面加,也可以往head的后面加,相当于加到链表头和链表尾。初始情况下head的前后指针指向自己。链表中的结点就是定时器结点。

双向链表的代码如下:

#pragma once
/**
 * 循环双向链表
 */

// 链表结点
typedef struct clinknode {
    struct clinknode *next;
    struct clinknode *prev;
} clinknode_t;

// 初始化链表头:前后都指向自己
static inline void clinklist_init(clinknode_t *head) {
    head->prev = head;
    head->next = head;
}

// 插入结点到链表的前面,因为是循环链表,其实是在head的后面
static inline void clinklist_add_front(clinknode_t *head, clinknode_t *node) {
    node->prev = head;
    node->next = head->next;
    head->next->prev = node;
    head->next = node;
}

// 插入结点到链表的后面,因为是循环链表,所以其实是在head的前面
static inline void clinklist_add_back(clinknode_t *head, clinknode_t *node) {
    node->prev = head->prev;
    node->next = head;
    node->prev->next = node;
    head->prev = node;
}

// 判断链表是否为空:循环链表为空是头的下一个和上一个都指向自己
static inline int clinklist_is_empty(clinknode_t *head) {
    return head == head->next;
}

// 从链表中移除自己,同时会重设结点
static inline void clinklist_remote(clinknode_t *node) {
    node->next->prev = node->prev;
    node->prev->next = node->next;
    clinklist_init(node);
}

// 将链表1的结点取出来,放到链表2
static inline void clinklist_splice(clinknode_t *head1, clinknode_t *head2) {
    if (!clinklist_is_empty(head1)) {
        clinknode_t *first = head1->next;       // 第1个结点
        clinknode_t *last = head1->prev;        // 最后1个结点
        clinknode_t *at = head2->next;          // 插在第2个链表的这个结点前面
        first->prev = head2;
        head2->next = first;
        last->next = at;
        at->prev = last;
        clinklist_init(head1);
    }
}

整个定时器的代码如下:

timerwheel.h

#pragma once
/**
 * 定时器模块
 */
#include 
#include 
#include 
#include 
#include "clinklist.h"


// 时间轮定时器

// 第1个轮占的位数
#define TVR_BITS 8
// 第1个轮的长度
#define TVR_SIZE (1 <// 第n个轮占的位数#define TVN_BITS 6// 第n个轮的长度#define TVN_SIZE (1 <// 掩码:取模或整除用#define TVR_MASK (TVR_SIZE - 1)#define TVN_MASK (TVN_SIZE - 1)// 定时器回调函数typedef void (*timer_cb_t)(void*);// 定时器结点typedef struct timernode {struct linknode *next;        // 下一个结点struct linknode *prev;        // 上一个结点void *userdata;               // 用户数据timer_cb_t callback;          // 回调函数uint32_t expire;              // 到期时间
} timernode_t;// 第1个轮typedef struct tvroot {clinknode_t vec[TVR_SIZE];
} tvroot_t;// 后面几个轮typedef struct tvnum {clinknode_t vec[TVN_SIZE];
} tvnum_t;// 时间轮定时器typedef struct timerwheel {tvroot_t tvroot;               // 第1个轮tvnum_t tv[4];                 // 后面4个轮uint64_t lasttime;             // 上一次的时间毫秒uint32_t currtick;             // 当前的tickuint16_t interval;             // 每个时间点的毫秒间隔uint16_t remainder;            // 剩余的毫秒
} timerwheel_t;// 初始化时间轮,interval为每帧的间隔,currtime为当前时间void timerwheel_init(timerwheel_t *tw, uint16_t interval, uint64_t currtime);// 初始化时间结点:cb为回调,ud为用户数据void timerwheel_node_init(timernode_t *node, timer_cb_t cb, void *ud);// 增加时间结点,ticks为触发间隔(注意是以interval为单位)void timerwheel_add(timerwheel_t *tw, timernode_t *node, uint32_t ticks);// 删除结点int timerwheel_del(timerwheel_t *tw, timernode_t *node);// 更新时间轮void timerwheel_update(timerwheel_t *tw, uint64_t currtime);

timerwheel.c

#include "timerwheel.h"

#define FIRST_INDEX(v) ((v) & TVR_MASK)
#define NTH_INDEX(v, n) (((v) >> (TVR_BITS + (n) * TVN_BITS)) & TVN_MASK)

void timerwheel_init(timerwheel_t *tw, uint16_t interval, uint64_t currtime) {
    memset(tw, 0, sizeof(*tw));
    tw->interval = interval;
    tw->lasttime = currtime;
    int i, j;
    for (i = 0; i         clinklist_init(tw->tvroot.vec + i);
    }
    for (i = 0; i 4; ++i) {
        for (j = 0; j             clinklist_init(tw->tv[i].vec + j);
    }
}

void timerwheel_node_init(timernode_t *node, timer_cb_t cb, void *ud) {
    node->next = 0;
    node->prev = 0;
    node->userdata = ud;
    node->callback = cb;
    node->expire = 0;
}

static void _timerwheel_add(timerwheel_t *tw, timernode_t *node) {
    uint32_t expire = node->expire;
    uint32_t idx = expire - tw->currtick;
    clinknode_t *head;
    if (idx         head = tw->tvroot.vec + FIRST_INDEX(expire);
    } else {
        int i;
        uint64_t sz;
        for (i = 0; i 4; ++i) {
            sz = (1ULL <1) * TVN_BITS));
            if (idx                 idx = NTH_INDEX(expire, i);
                head = tw->tv[i].vec + idx;
                break;
            }
        }
    }
    clinklist_add_back(head, (clinknode_t*)node);
}

void timerwheel_add(timerwheel_t *tw, timernode_t *node, uint32_t ticks) {
    node->expire = tw->currtick + ((ticks > 0) ? ticks : 1);
    _timerwheel_add(tw, node);
}

int timerwheel_del(timerwheel_t *tw, timernode_t *node) {
    if (!clinklist_is_empty((clinknode_t*)node)) {
        clinklist_remote((clinknode_t*)node);
        return 1;
    }
    return 0;
}

void _timerwheel_cascade(timerwheel_t *tw, tvnum_t *tv, int idx) {
    clinknode_t head;
    clinklist_init(&head);
    clinklist_splice(tv->vec + idx, &head);
    while (!clinklist_is_empty(&head)) {
        timernode_t *node = (timernode_t*)head.next;
        clinklist_remote(head.next);
        _timerwheel_add(tw, node);
    }
}

void _timerwheel_tick(timerwheel_t *tw) {
    ++tw->currtick;

    uint32_t currtick = tw->currtick;
    int index = (currtick & TVR_MASK); 
    if (index == 0) {
        int i = 0;
        int idx;
        do {
            idx = NTH_INDEX(tw->currtick, i);
            _timerwheel_cascade(tw, &(tw->tv[i]), idx);
        } while (idx == 0 && ++i 4);
    }

    clinknode_t head;
    clinklist_init(&head);
    clinklist_splice(tw->tvroot.vec + index, &head);
    while (!clinklist_is_empty(&head)) {
        timernode_t *node = (timernode_t*)head.next;
        clinklist_remote(head.next);
        if (node->callback) {
            node->callback(node->userdata);
        }
    }
}

void timerwheel_update(timerwheel_t *tw, uint64_t currtime) {
    if (currtime > tw->lasttime) {
        int diff = currtime - tw->lasttime + tw->remainder;
        int intv = tw->interval;
        tw->lasttime = currtime;

        while (diff >= intv) {
            diff -= intv;
            _timerwheel_tick(tw);
        }
        tw->remainder = diff;
    }
}

核心函数就两个:

  • _timerwheel_add 把定时器结点加到时间轮里。
  • _timerwheel_tick 指针往前走一步,如果指针到达0,则上一个圈的指针也会跳一步,此时要把上一个圈的链表取出来,重新加到当前圈里(_timerwheel_cascade)

这份代码参考了2.6内核的timer,其中涉及到一些位运算,不过通过上面的讲解,应该不难理解。有兴趣了解的还是看代码吧,代码才是最好的文档:)

- EOF -