[libco] libco 定时器(时间轮)

libco 定时器核心数据结构:数组 + 链表,有点像哈希表,通过空间换时间。libco 定时器也被称为时间轮,我们看看这个 “轮” 是怎么转的。

文章来源: [libco] libco 定时器(时间轮)


1. 概述

libco 定时器核心数据结构:数组 + 双向链表(左图)。

数组以毫秒为单位,默认大小 60 * 1000,主要保存一分钟以内到期的事件数据。相同到期时间的事件,会保存在双向链表里,当时间到期时,到期事件链表会一起取出来。

当然超过一分钟的到期事件也支持保存,通过取模路由,有可能与一分钟以内到期的数据耦合在一起,一起取出来后,再检查,没到期的重新写回去即可。

一般的应用场景,超时事件也是 1 分钟以内,超过这个时间事件的处理方案,虽然有点笨,但是代码维护代价比较小。


libco 定时器也被称为时间轮(右图):因为数组数据结构,下标是以毫秒为单位,当前时间下标 stTimeout_t.llStartIdx,沿着这个下标,随着时间推移,顺时针读写数据。

图片来源:processon


2. 定时器源码分析

  • 数据结构。
struct stCoEpoll_t {
    ...
    struct stTimeout_t *pTimeout; /* 定时器。 */
    ...
};

/* 定时器。 */
struct stTimeout_t {
    stTimeoutItemLink_t *pItems; /* 数组。 */
    int iItemSize;               /* 数组大小。 */
    unsigned long long ullStart; /* 上一次获取到期事件的时间。 */
    long long llStartIdx;        /* 上一次获取到期事件的数组下标。 */
};

/* 到期事件双向链表。 */
struct stTimeoutItemLink_t {
    stTimeoutItem_t *head;
    stTimeoutItem_t *tail;
};
  • 初始化定时器数据结构,数组大小 60000。一般应用场景,定时任务都在 1 分钟(60000 毫秒)以内,那么超过一分钟的怎么办?看下面 AddTimeout 的源码实现。
stCoEpoll_t *AllocEpoll() {
    ...
    ctx->pTimeout = AllocTimeout(60 * 1000);
    ...
}

stTimeout_t *AllocTimeout(int iSize) {
    stTimeout_t *lp = (stTimeout_t *)calloc(1, sizeof(stTimeout_t));
    lp->iItemSize = iSize;
    lp->pItems = (stTimeoutItemLink_t *)calloc(1, sizeof(stTimeoutItemLink_t) * lp->iItemSize);
    lp->ullStart = GetTickMS();
    lp->llStartIdx = 0;
    return lp;
}
  • 添加到期事件。
int AddTimeout(stTimeout_t *apTimeout, stTimeoutItem_t *apItem, unsigned long long allNow) {
    ...
    unsigned long long diff = apItem->ullExpireTime - apTimeout->ullStart;
    if (diff >= (unsigned long long)apTimeout->iItemSize) {
        /* 超过一分钟的事件,应该排在轮子尾部,最后才检查的,
         * 因为轮子是“圆”的,所以尾部就是当前数组下标(llStartIdx)的前一个下标。
         * (这里有点绕...) */
        diff = apTimeout->iItemSize - 1;
        ...
    }

    /* 通过取模,将定时器事件存储到数组对应的双向链表里。 */
    AddTail(apTimeout->pItems + (apTimeout->llStartIdx + diff) % apTimeout->iItemSize, apItem);
    return 0;
}
  • 获取到期事件。
/* 获取到期事件。 */
inline void TakeAllTimeout(stTimeout_t *apTimeout, unsigned long long allNow, stTimeoutItemLink_t *apResult) {
    ...
    /* 处理当前时间与上一次处理时间间隔内到期的时间事件。 */
    int cnt = allNow - apTimeout->ullStart + 1;
    if (cnt > apTimeout->iItemSize) {
        cnt = apTimeout->iItemSize;
    }
    ...
    for (int i = 0; i < cnt; i++) {
        int idx = (apTimeout->llStartIdx + i) % apTimeout->iItemSize;
        Join<stTimeoutItem_t, stTimeoutItemLink_t>(apResult, apTimeout->pItems + idx);
    }
    /* 刷新当前数据。 */
    apTimeout->ullStart = allNow;
    apTimeout->llStartIdx += cnt - 1;
}

/* 事件循环,处理事件。 */
void co_eventloop(stCoEpoll_t *ctx, pfn_co_eventloop_t pfn, void *arg) {
    ...
    for (;;) {
        int ret = co_epoll_wait(ctx->iEpollFd, result, stCoEpoll_t::_EPOLL_SIZE, 1);
        ...
        /* 取出当前时间到期事件。 */
        unsigned long long now = GetTickMS();
        TakeAllTimeout(ctx->pTimeout, now, timeout);
        ...
        /* 标识这个时间是到期事件,因为 fd 事件和时间事件耦合在一起了(!^_^)。 */
        stTimeoutItem_t *lp = timeout->head;
        while (lp) {
            lp->bTimeout = true;
            lp = lp->pNext;
        }

        Join<stTimeoutItem_t, stTimeoutItemLink_t>(active, timeout);

        lp = active->head;
        while (lp) {
            /* 遍历处理活跃事件。(fd 事件,到期事件。) */
            PopHead<stTimeoutItem_t, stTimeoutItemLink_t>(active);
            /* 处理时间时间,到期的先处理,没到期的重新添加回去。 */
            if (lp->bTimeout && now < lp->ullExpireTime) {
                /* 因为时间轮数组只有 60000 个下标,一般存储 1 分钟以内的到期事件,
                 * 但是超过 1 分钟到期的事件也支持存储,这样有可能这些事件经过取模,
                 * 与 1 分钟以内到期事件存储在同一个双向链表里,被取出来了,
                 * 所以这些没到期的事件,应该重新存回去。 */
                int ret = AddTimeout(ctx->pTimeout, lp, now);
                if (!ret) {
                    lp->bTimeout = false;
                    lp = active->head;
                    continue;
                }
            }
            ...
            lp = active->head;
        }
    }
}

3. 缺点

  • 时间轮数组默认大小是 60 * 1000。以毫秒为单位,最大时间间隔是一分钟,但是如果处理超过一分钟的过期事件,处理效率就降低了,例如 session,它的过期时间就经常超过 1 分钟。
  • libco 的定时器设计主要是为了它内部协程切换使用,添加一个定时事件以后,很难通过查找方式,删除指定时间事件,只有等到事件触发了,事件才会从双向链表中删除。
  • 定时器对外部使用不友好,貌似只有创建协程,在协程处理函数内部通过 poll 实现。而且通过协程去实现定时器,成本太大了,一个协程结构体就几 K 的内存空间 😔,所以我自己在处理 session 过期问题,不得不造了个轻量级的定时器轮子:基于 stl map 的定时器
  • 综合上述几个问题,这也很好解析了,为啥有的开源通过小堆去维护定时器,例如 libev。

4. 小结

libco 的定时器设计非常优秀,在一定的范围内,非常高效,虽然有些小缺点,但瑕不掩瑜。


5. 参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容