多级时间轮定时器:原理与C语言实现

张开发
2026/4/4 4:22:19 15 分钟阅读
多级时间轮定时器:原理与C语言实现
1. 多级时间轮定时器设计与实现在嵌入式系统和服务器开发中定时器管理是一个基础但至关重要的功能。传统链表式定时器在任务量增大时性能会急剧下降而多级时间轮算法则能保持稳定的O(1)时间复杂度。本文将详细解析一个基于C语言实现的多级时间轮定时器它参考了Linux内核的实现思路支持长达49天的定时范围0~2^32毫秒。这个实现采用了五级时间轮结构通过位运算高效计算时间槽位置使用双向链表管理定时任务。下面我们从设计原理到具体实现逐步拆解这个系统。2. 核心数据结构解析2.1 多级时间轮框架时间轮的基本原理类似于钟表的运转方式秒针转一圈60秒分针移动一格1分钟分针转一圈60分钟时针移动一格1小时同理在我们的五级时间轮中第1级工作轮转一圈第2级移动一格第2级转一圈第3级移动一格以此类推这种级联设计使得我们可以用有限的空间管理大范围的定时任务。只有最底层的工作轮tv1上的任务会被立即执行上层轮的任务会逐步下放到工作轮。2.2 时间轮对象定义#define TVN_BITS 6 #define TVR_BITS 8 #define TVN_SIZE (1TVN_BITS) // 64 #define TVR_SIZE (1TVR_BITS) // 256 struct tvec { struct list_head vec[TVN_SIZE]; // 64个槽位 }; struct tvec_root { struct list_head vec[TVR_SIZE]; // 256个槽位 }; struct tvec_base { unsigned long current_index; // 当前指针位置 pthread_t thincrejiffies; // 时间推进线程 pthread_t threadID; // 工作线程 struct tvec_root tv1; // 第1级轮工作轮 struct tvec tv2; // 第2级轮 struct tvec tv3; // 第3级轮 struct tvec tv4; // 第4级轮 struct tvec tv5; // 第5级轮 };这个设计有几个关键点工作轮tv1有256个槽位其他轮有64个槽位current_index记录当前时间轮的绝对时间单位是毫秒通过位运算可以快速定位任务应该放在哪个轮的哪个槽位2.3 定时任务对象typedef void (*timeouthandle)(unsigned long); struct timer_list { struct list_head entry; // 链表节点 unsigned long expires; // 到期时间 void (*function)(unsigned long); // 回调函数 unsigned long data; // 回调参数 struct tvec_base *base; // 所属时间轮 };每个定时任务包含链表节点用于挂载到时间轮的槽位到期时间绝对时间戳毫秒回调函数到期时执行的函数参数传递给回调函数的数据3. 核心算法实现3.1 任务添加机制static void internal_add_timer(struct tvec_base *base, struct timer_list *timer) { struct list_head *vec; unsigned long expires timer-expires; unsigned long idx expires - base-current_index; if ((signed long)idx 0) { // 已经过期的任务放到工作轮当前槽 vec base-tv1.vec (base-current_index TVR_MASK); } else if (idx TVR_SIZE) { // 第1级轮 int i expires TVR_MASK; vec base-tv1.vec i; } else if (idx 1 (TVR_BITS TVN_BITS)) { // 第2级轮 int i (expires TVR_BITS) TVN_MASK; vec base-tv2.vec i; } // ... 类似处理第3、4、5级轮 else { // 超过最大范围的任务放到第5级轮最后槽 if (idx 0xffffffffUL) idx 0xffffffffUL; int i (expires (TVR_BITS 3 * TVN_BITS)) TVN_MASK; vec base-tv5.vec i; } list_add_tail(timer-entry, vec); }这个函数的关键点计算任务到期时间与当前时间的差值idx根据idx大小决定放入哪一级时间轮使用位运算快速定位具体槽位将任务添加到对应槽位的链表尾部3.2 时间推进与任务触发static void *deal_function_timeout(void *base) { struct timer_list *timer; struct tvec_base *ba (struct tvec_base *)base; for (;;) { struct timeval tv; gettimeofday(tv, NULL); unsigned long now tv.tv_sec * 1000 tv.tv_usec / 1000; while (ba-current_index now) { int index ba-current_index TVR_MASK; struct list_head work_list; // 处理级联 if (!index) { cascade(ba, ba-tv2, INDEX(0)); cascade(ba, ba-tv3, INDEX(1)); cascade(ba, ba-tv4, INDEX(2)); cascade(ba, ba-tv5, INDEX(3)); } ba-current_index; list_replace_init(ba-tv1.vec index, work_list); // 执行到期任务 while (!list_empty(work_list)) { timer list_first_entry(work_list, struct timer_list, entry); detach_timer(timer); timer-function(timer-data); } } usleep(1000); // 休眠1ms避免CPU占用过高 } }这个核心循环的工作流程获取当前系统时间毫秒推进时间轮指针处理所有到期的槽位当工作轮完成一圈时触发级联操作执行到期任务回调函数3.3 级联操作实现static int cascade(struct tvec_base *base, struct tvec *tv, int index) { struct list_head *pos, *tmp; struct timer_list *timer; struct list_head tv_list; // 将当前槽位的任务转移到临时链表 list_replace_init(tv-vec index, tv_list); // 重新分配这些任务到合适的位置 list_for_each_safe(pos, tmp, tv_list) { timer list_entry(pos, struct timer_list, entry); internal_add_timer(base, timer); } return index; }级联操作的关键点将上层轮某个槽位的所有任务取出根据当前时间重新计算这些任务的位置可能下放到更底层的轮也可能留在当前轮4. 使用示例与API说明4.1 基本使用流程// 定时器回调函数示例 void mytimer(unsigned long arg) { struct request_para *para (struct request_para *)arg; printf(Timer fired: %d\n, para-val); // 可以在这里重新设置定时器 mod_timer(para-timer, 3000); } int main() { // 创建时间轮 void *pwheel ti_timewheel_create(); // 准备定时器参数 struct request_para *para malloc(sizeof(struct request_para)); para-val 100; // 添加3秒定时器 para-timer ti_add_timer(pwheel, 3000, mytimer, (unsigned long)para); // 主循环 while (1) { sleep(1); } // 释放资源 ti_timewheel_release(pwheel); return 0; }4.2 主要API接口创建时间轮void *ti_timewheel_create(void);初始化五级时间轮结构创建后台线程处理定时任务返回时间轮句柄添加定时器void *ti_add_timer(void *ptimewheel, unsigned long expires, timeouthandle phandle, unsigned long arg);ptimewheel: 时间轮句柄expires: 超时时间毫秒phandle: 回调函数arg: 回调参数返回定时器句柄修改定时器int mod_timer(void *ptimer, unsigned long expires);修改已有定时器的超时时间自动重新计算位置删除定时器void ti_del_timer(void *p);从时间轮中移除定时器释放相关资源释放时间轮void ti_timewheel_release(void *pwheel);停止工作线程释放所有定时器资源销毁时间轮结构5. 性能分析与优化建议5.1 时间复杂度分析添加定时器O(1)通过位运算直接定位槽位链表操作是常数时间触发定时器O(1) per tick每个时钟tick只需处理当前槽位的任务级联操作分摊后也是O(1)删除定时器O(1)双向链表支持快速删除5.2 已知问题与解决方案长任务阻塞问题现象如果一个任务执行时间过长会延迟后续任务的执行解决方案将耗时任务移到工作线程池执行设置任务最大执行时间超时强制中断时间精度问题当前实现依赖系统时钟最小精度1ms高精度需求场景可考虑使用timerfd或硬件定时器多线程安全问题当前实现未加锁多线程操作需要额外同步建议添加读写锁保护关键数据结构5.3 扩展优化方向动态时间轮当前槽位数量固定可以改为动态调整根据负载自动增加或减少轮数分层时间轮不同精度需求的任务放到不同层级高精度任务用高频率轮低精度任务用低频率轮批量操作优化支持批量添加/删除定时器减少锁竞争开销6. 实现细节与技巧6.1 高效位运算时间轮的核心优势在于使用位运算快速定位槽位// 计算第N级轮的槽位索引 #define INDEX(N) ((ba-current_index (TVR_BITS (N) * TVN_BITS)) TVN_MASK) // 工作轮槽位计算 int index ba-current_index TVR_MASK; // 第2级轮槽位计算 int i (expires TVR_BITS) TVN_MASK;这种计算方式避免了昂贵的除法和取模运算。6.2 双向链表实现使用Linux内核风格的通用双向链表实现struct list_head { struct list_head *next, *prev; }; // 初始化链表头 #define LIST_HEAD_INIT(name) { (name), (name) } // 添加节点到链表尾部 static inline void list_add_tail(struct list_head *entry, struct list_head *head) { __list_add(entry, head-prev, head); }这种实现的特点是通用性强可以嵌入任何结构体通过container_of宏可以获取包含链表的结构体指针提供丰富的操作接口6.3 时间处理技巧时间获取优化使用gettimeofday()获取当前时间转换为毫秒时间戳统一处理struct timeval tv; gettimeofday(tv, NULL); unsigned long now tv.tv_sec * 1000 tv.tv_usec / 1000;时间比较处理处理时间回跳问题如NTP调整使用有符号比较判断时间差if ((signed long)(expires - base-current_index) 0) { // 已经过期的任务 }7. 测试与验证7.1 功能测试基本定时功能void test_timer(unsigned long arg) { static int count 0; printf(Timer %d fired\n, count); } // 添加1秒间隔的定时器 ti_add_timer(wheel, 1000, test_timer, 0);定时器重置测试void restart_timer(unsigned long arg) { struct timer_list *timer (struct timer_list *)arg; printf(Timer fired, restarting...\n); mod_timer(timer, 2000); // 2秒后再次触发 }长时间定时测试void long_term_timer(unsigned long arg) { printf(24 hour timer fired!\n); } // 设置24小时定时器 ti_add_timer(wheel, 24*3600*1000, long_term_timer, 0);7.2 性能测试批量定时器测试#define TEST_COUNT 10000 void batch_callback(unsigned long arg) { // 空回调用于性能测试 } // 添加10000个定时器 for (int i 0; i TEST_COUNT; i) { ti_add_timer(wheel, 1000 i, batch_callback, 0); }高密度定时测试// 添加100个1ms间隔的定时器 for (int i 0; i 100; i) { ti_add_timer(wheel, i1, fast_callback, 0); }测试要点内存使用情况CPU占用率定时精度统计任务触发延迟8. 实际应用建议8.1 适用场景网络编程TCP超时重传连接保活检测请求超时处理游戏开发技能冷却计时动画效果定时游戏状态更新嵌入式系统传感器轮询设备状态检测周期性任务调度8.2 使用注意事项回调函数设计保持短小精悍避免长时间运行不要阻塞或执行耗时操作避免在回调中申请大量内存资源管理定时器对象需要手动释放注意回调参数的生命周期时间轮销毁前确保所有定时器已处理线程安全默认实现不是线程安全的多线程环境需要额外同步考虑使用读写锁保护时间轮8.3 调试技巧日志输出在关键路径添加调试日志记录定时器的添加、触发和删除统计监控跟踪各层级时间轮的任务数量监控任务执行时间分布压力测试模拟极端情况下的行为测试内存不足时的处理验证长时间运行的稳定性这个多级时间轮实现提供了高效的定时器管理能力特别适合需要管理大量定时任务的场景。通过理解其设计原理和实现细节开发者可以根据具体需求进行定制和优化。

更多文章