libuv/事件循环定时器

从定时器堆确定等待时间,再集中执行到期回调

libuv 用侵入式二叉最小堆索引定时器;到期项先移入就绪队列,再重新设置周期并调用回调。

eb497c582调度与资源分配核验 2026-10-12
相关概念

工程问题#

事件循环同时等待 I/O 与多个定时器。每次等待前需要知道最近的期限,回调又可能停止、重启或修改其他定时器,不能把回调执行与堆顶遍历随意混在一起。

实际实现#

uv_timer_start 先移除旧状态,再按 timeout、start_id 入堆。堆节点嵌在 handle 中,通过父子指针维护完全二叉树。uv__next_timeout 读取堆顶计算等待上限;uv__run_timers 先收集到期 handle 到 ready_queue,然后逐个出队、uv_timer_again 并调用回调。Unix 事件循环仅在没有其他立即工作时使用这个等待上限。

处理流程#

  1. 启动或重启时计算期限,以启动序号打破相同期限的比较平局
  2. 把嵌入 handle 的节点插入最小堆,并通过交换恢复堆序
  3. 读取最早期限,确定 I/O 最多等待多久
  4. 把当前已到期项从堆移到就绪队列,再逐个重设周期并执行回调

源码片段#

固定版本 · 原文摘录
src/timer.ceb497c582f5c0887add4d4658c7801e4ef412e10 · L37–L54
查看原文件
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}
timer_less_than

期限相同时比较启动序号;节点通过 container_of 找回定时器 handle。

src/timer.ceb497c582f5c0887add4d4658c7801e4ef412e10 · L76–L93
查看原文件
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;
uv_timer_start

先停止旧状态,计算期限并记录 repeat,然后插入定时器堆。

src/heap-inl.heb497c582f5c0887add4d4658c7801e4ef412e10 · L127–L148
查看原文件
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}
heap_insert

沿预先计算的路径找到完全二叉树插入点,再向上交换恢复堆序。

src/timer.ceb497c582f5c0887add4d4658c7801e4ef412e10 · L143–L160
查看原文件
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;
uv__next_timeout

最早期限决定等待上限,空堆返回 -1;毫秒差被限制到 INT_MAX。

src/timer.ceb497c582f5c0887add4d4658c7801e4ef412e10 · L172–L193
查看原文件
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  }
uv__run_timers

先收集到期项,再逐个出队、重设周期和调用回调。

src/unix/core.ceb497c582f5c0887add4d4658c7801e4ef412e10 · L403–L413
查看原文件
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}
uv__backend_timeout

存在待处理、idle、close 等立即工作时返回零,不会只依据定时器休眠。

摘录分别保留调用入口、核心机制及边界;完整上下文见各段源码链接。

收益与取舍#

工程分析

带来的收益

  • 由代码结构可知,堆顶访问为常数成本,插入与移除沿树高执行;无需每轮扫描全部定时器
  • 堆节点直接复用 handle 内存,就绪阶段与定时器索引维护分开

付出的代价

  • 插入与移除仍有对数级维护成本;这份实现是指针式二叉堆,不是可扩容数组
  • 周期定时器按事件循环当前时间重新设置;慢回调和其他工作会造成延迟,不能当作硬实时或逐次补发保证
  • stop、restart 及回调内操作会影响后续就绪项,阅读时必须保留队列节点初始化和移除规则

可比较的选择

有序链表读取首项直接,但寻找插入位置可能遍历大量定时器。

时间轮适合批量普通超时,需要明确时间粒度、跨槽处理和期限范围。

版本与适用范围#

固定到提交 eb497c582f5c。分析 timer.c 及 Unix 等待入口,未声称所有平台事件循环顺序完全相同;同期限比较使用 start_id,周期重设发生在回调之前。复杂度只针对索引维护,不包括回调执行。

参考来源#

源码、官方文档与原始论文
01
libuv · timer_less_than

src/timer.c · timer_less_than

源码eb497c582f5c0887add4d4658c7801e4ef412e10
02
libuv · heap_insert

src/heap-inl.h · heap_insert

源码eb497c582f5c0887add4d4658c7801e4ef412e10
03
libuv · uv__backend_timeout

src/unix/core.c · uv__backend_timeout

源码eb497c582f5c0887add4d4658c7801e4ef412e10
04官方文档官方文档;实现以固定提交为准

相关实现与比较#

4 个相关案例
nginx·事件循环release-1.26.3

nginx 事件定时器以到期时间为键放入红黑树,事件循环读取最小键确定等待上限。

附源码片段 · 调度与资源分配
systemd·.timer 单元与 sd-event 时间事件v257

systemd 的 sd-event 为每个时钟维护两个带位置索引的小顶堆,快速找到下一次到期时间,并利用允许的延迟余量合并唤醒。

附源码片段 · 调度与资源分配
SwarmKit·服务调度器1fd637ba5

SwarmKit 的调度决策树在叶子中使用 nodeMaxHeap,把候选节点整理为从优到劣的顺序。

附源码片段 · 调度与资源分配
19 个匹配案例