从定时器堆确定等待时间,再集中执行到期回调
libuv 用侵入式二叉最小堆索引定时器;到期项先移入就绪队列,再重新设置周期并调用回调。
工程问题#
事件循环同时等待 I/O 与多个定时器。每次等待前需要知道最近的期限,回调又可能停止、重启或修改其他定时器,不能把回调执行与堆顶遍历随意混在一起。
实际实现#
uv_timer_start 先移除旧状态,再按 timeout、start_id 入堆。堆节点嵌在 handle 中,通过父子指针维护完全二叉树。uv__next_timeout 读取堆顶计算等待上限;uv__run_timers 先收集到期 handle 到 ready_queue,然后逐个出队、uv_timer_again 并调用回调。Unix 事件循环仅在没有其他立即工作时使用这个等待上限。
处理流程#
- 启动或重启时计算期限,以启动序号打破相同期限的比较平局
- 把嵌入 handle 的节点插入最小堆,并通过交换恢复堆序
- 读取最早期限,确定 I/O 最多等待多久
- 把当前已到期项从堆移到就绪队列,再逐个重设周期并执行回调
源码片段#
固定版本 · 原文摘录37static int timer_less_than(const struct heap_node* ha,38 const struct heap_node* hb) {39 const uv_timer_t* a;40 const uv_timer_t* b;4142 a = container_of(ha, uv_timer_t, node.heap);43 b = container_of(hb, uv_timer_t, node.heap);4445 if (a->timeout < b->timeout)46 return 1;47 if (b->timeout < a->timeout)48 return 0;4950 /* Compare start_id when both have the same timeout. start_id is51 * allocated with loop->timer_counter in uv_timer_start().52 */53 return a->start_id < b->start_id;54}期限相同时比较启动序号;节点通过 container_of 找回定时器 handle。
76 uv_timer_stop(handle);7778 clamped_timeout = handle->loop->time + timeout;79 if (clamped_timeout < timeout)80 clamped_timeout = (uint64_t) -1;8182 handle->timer_cb = cb;83 handle->timeout = clamped_timeout;84 handle->repeat = repeat;85 /* start_id is the second index to be compared in timer_less_than() */86 handle->start_id = handle->loop->timer_counter++;8788 heap_insert(timer_heap(handle->loop),89 (struct heap_node*) &handle->node.heap,90 timer_less_than);91 uv__handle_start(handle);9293 return 0;先停止旧状态,计算期限并记录 repeat,然后插入定时器堆。
127 parent = child = &heap->min;128 while (k > 0) {129 parent = child;130 if (path & 1)131 child = &(*child)->right;132 else133 child = &(*child)->left;134 path >>= 1;135 k -= 1;136 }137138 /* Insert the new node. */139 newnode->parent = *parent;140 *child = newnode;141 heap->nelts += 1;142143 /* Walk up the tree and check at each node if the heap property holds.144 * It's a min heap so parent < child must be true.145 */146 while (newnode->parent != NULL && less_than(newnode, newnode->parent))147 heap_node_swap(heap, newnode->parent, newnode);148}沿预先计算的路径找到完全二叉树插入点,再向上交换恢复堆序。
143int uv__next_timeout(const uv_loop_t* loop) {144 const struct heap_node* heap_node;145 const uv_timer_t* handle;146 uint64_t diff;147148 heap_node = heap_min(timer_heap(loop));149 if (heap_node == NULL)150 return -1; /* block indefinitely */151152 handle = container_of(heap_node, uv_timer_t, node.heap);153 if (handle->timeout <= loop->time)154 return 0;155156 diff = handle->timeout - loop->time;157 if (diff > INT_MAX)158 diff = INT_MAX;159160 return (int) diff;最早期限决定等待上限,空堆返回 -1;毫秒差被限制到 INT_MAX。
172 for (;;) {173 heap_node = heap_min(timer_heap(loop));174 if (heap_node == NULL)175 break;176177 handle = container_of(heap_node, uv_timer_t, node.heap);178 if (handle->timeout > loop->time)179 break;180181 uv_timer_stop(handle);182 uv__queue_insert_tail(&ready_queue, &handle->node.queue);183 }184185 while (!uv__queue_empty(&ready_queue)) {186 queue_node = uv__queue_head(&ready_queue);187 uv__queue_remove(queue_node);188 uv__queue_init(queue_node);189 handle = container_of(queue_node, uv_timer_t, node.queue);190191 uv_timer_again(handle);192 handle->timer_cb(handle);193 }先收集到期项,再逐个出队、重设周期和调用回调。
403static int uv__backend_timeout(const uv_loop_t* loop) {404 if (loop->stop_flag == 0 &&405 /* uv__loop_alive(loop) && */406 (uv__has_active_handles(loop) || uv__has_active_reqs(loop)) &&407 uv__queue_empty(&loop->pending_queue) &&408 uv__queue_empty(&loop->idle_handles) &&409 (loop->flags & UV_LOOP_REAP_CHILDREN) == 0 &&410 loop->closing_handles == NULL)411 return uv__next_timeout(loop);412 return 0;413}存在待处理、idle、close 等立即工作时返回零,不会只依据定时器休眠。
摘录分别保留调用入口、核心机制及边界;完整上下文见各段源码链接。
收益与取舍#
工程分析带来的收益
- 由代码结构可知,堆顶访问为常数成本,插入与移除沿树高执行;无需每轮扫描全部定时器
- 堆节点直接复用 handle 内存,就绪阶段与定时器索引维护分开
付出的代价
- 插入与移除仍有对数级维护成本;这份实现是指针式二叉堆,不是可扩容数组
- 周期定时器按事件循环当前时间重新设置;慢回调和其他工作会造成延迟,不能当作硬实时或逐次补发保证
- stop、restart 及回调内操作会影响后续就绪项,阅读时必须保留队列节点初始化和移除规则
可比较的选择
有序链表读取首项直接,但寻找插入位置可能遍历大量定时器。
时间轮适合批量普通超时,需要明确时间粒度、跨槽处理和期限范围。
版本与适用范围#
固定到提交 eb497c582f5c。分析 timer.c 及 Unix 等待入口,未声称所有平台事件循环顺序完全相同;同期限比较使用 start_id,周期重设发生在回调之前。复杂度只针对索引维护,不包括回调执行。
参考来源#
源码、官方文档与原始论文src/unix/core.c · uv__backend_timeout
相关实现与比较#
4 个相关案例nginx 事件定时器以到期时间为键放入红黑树,事件循环读取最小键确定等待上限。
systemd 的 sd-event 为每个时钟维护两个带位置索引的小顶堆,快速找到下一次到期时间,并利用允许的延迟余量合并唤醒。
SwarmKit 的调度决策树在叶子中使用 nodeMaxHeap,把候选节点整理为从优到劣的顺序。
Linux IIO 的 kfifo 缓冲在采样进入内核与用户态读取之间暂存数据,按先进先出顺序交付采样记录。