ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

libuv 定时器句柄 uv_timer_t 完全指南:API 详解与事件循环调度原理

libuv 定时器句柄 uv_timer_t 完全指南:API 详解与事件循环调度原理 libuv 定时器句柄 uv_timer_t 完全指南API 详解与事件循环调度原理【免费下载链接】libuvCross-platform asynchronous I/O项目地址: https://gitcode.com/gh_mirrors/li/libuv本文以 docs/src/timer.rst 为骨架结合 libuv 源码与测试系统讲解uv_timer_t定时器句柄的使用方法、API 语义与底层调度机制。读完你将掌握如何创建单次/重复定时器、理解重复定时器相对 now 重排的精确语义、uv_timer_again与uv_timer_set_repeat的差异以及定时器在事件循环迭代中的执行时机。概述定时器在 libuv 中的角色uv_timer_t是 libuv 中用于在未来的某个时刻调度回调执行的句柄类型。它是 libuv 事件循环中唯一以时间为触发条件的句柄无论是实现超时控制、心跳检测、轮询任务还是延迟执行定时器都是最直接的工具。与之配套的还有uv_prepare_t/uv_check_t在每次循环迭代的 I/O 轮询前后触发的句柄三者共同构成了基于时间与迭代节奏的调度体系相关对比可参考 docs/src/prepare.rst 与 docs/src/check.rst。定时器分为**单次single-shot与重复repeating**两种模式。一个关键语义是重复定时器不会为回调执行开销做补偿调整而是相对事件循环的now重新武装。libuv 会在两个时机更新自己对now的认知执行定时器回调之前从 I/O 等待中醒来之后。如需在任何时刻手动刷新这一概念可调用uv_update_time参见 docs/src/loop.rst 与 src/unix/internal.h。开销补偿语义示例原文档给出了一个精确的例子一个间隔为50 ms的重复定时器其回调执行耗时17 ms那么它将在33 ms 后再次运行50 - 17 33。如果其他任务占用了超过 33 ms 的时间则定时器回调会在尽快as soon as possible的时间点执行——即不会追赶进度也不会堆叠多次回调。这一行为直接体现在源码中uv__run_timers()在调用回调之前先以handle-repeat作为新的 timeout 重新启动定时器见下文底层调度一节从而天然实现了相对 now 重排。数据类型uv_timer_t定时器句柄类型在 include/uv.h 中定义为struct uv_timer_s { UV_HANDLE_FIELDS UV_TIMER_PRIVATE_FIELDS };公共成员N/A。所有内部状态回调指针、超时时间、重复间隔、堆节点等都通过UV_TIMER_PRIVATE_FIELDS隐藏属于平台私有字段。Unix 平台上的定义为include/uv/unix.h#define UV_TIMER_PRIVATE_FIELDS \ uv_timer_cb timer_cb; \ union { \ void* heap[3]; \ struct uv__queue queue; \ } node; \ uint64_t timeout; \ uint64_t repeat; \ uint64_t start_id;其中timeout存储的是绝对到期时刻loop-time 用户传入的 timeout而非相对时长start_id用于堆中排序的稳定性详见下文。所有 uv_handle_t 的公共成员 同样适用于uv_timer_t如loop、type、flags等uv_handle_t的 API 函数如uv_is_active、uv_close、uv_ref/uv_unref也全部适用。uv_timer_cbtypedef void (*uv_timer_cb)(uv_timer_t* handle);传递给uv_timer_start的回调函数类型定义。回调收到触发它的定时器句柄指针。API 详解int uv_timer_init(uv_loop_t* loop, uv_timer_t* handle)初始化定时器句柄。loop指定句柄所属的事件循环handle指向用户分配的uv_timer_t对象可以是栈上或堆上。从源码看src/timer.c初始化会完成句柄类型注册UV_TIMER、回调与超时字段清零、内部队列初始化并返回0。初始化后的句柄处于非活跃状态不会参与循环存活判定也不会触发任何回调。int uv_timer_start(uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat)启动定时器这是最核心的 API。参数语义参数含义handle已初始化的定时器句柄cb到期时触发的回调timeout首次触发前的延迟单位毫秒repeat后续重复触发的间隔单位毫秒为0表示单次定时器两条关键规则timeout为 0回调在下一次事件循环迭代触发并非立即、也非同步触发。测试 test/test-timer.c 的timer_zero_timeout用例验证了这一点回调中再次以(0, 0)启动自身并调用uv_stop最终确认回调只被调用一次。repeat非 0回调先在第timeout毫秒触发一次之后每隔repeat毫秒重复触发。注意两个重要约束不更新事件循环的now调用uv_timer_start不会刷新loop-time超时计算基于调用时的循环时钟。如需精确控制可先调用uv_update_time。重复启动即更新如果定时器已处于活跃状态再次调用uv_timer_start会静默地更新其超时与重复间隔源码中先执行uv_timer_stop再重新入堆。源码实现还展示了两个值得注意的细节src/timer.c若句柄正在关闭uv__is_closing或cb NULL返回UV_EINVALtimeout以loop-time timeout的绝对时刻存储若发生 uint64 溢出则钳制为(uint64_t)-1——这解释了为何timer_huge_timeout测试可以启动超时长达0xffffffffffff约 281 万亿毫秒的定时器test/test-timer.c。int uv_timer_stop(uv_timer_t* handle)停止定时器之后回调将不再被调用。源码src/timer.c展示了停止的两条路径活跃状态从最小堆中移除非活跃但已入队如刚从堆中摘出待回调则从就绪队列移除。停止后句柄变为非活跃但可以通过再次uv_timer_start重新启用。int uv_timer_again(uv_timer_t* handle)停止定时器若它是重复定时器则以 repeat 值作为新的 timeout 重新启动。典型用途是在定时器回调内部续期一个周期性任务。源码实现src/timer.cint uv_timer_again(uv_timer_t* handle) { if (handle-timer_cb NULL) return UV_EINVAL; if (handle-repeat) { uv_timer_stop(handle); uv_timer_start(handle, handle-timer_cb, handle-repeat, handle-repeat); } return 0; }若定时器从未被启动过timer_cb NULL返回UV_EINVAL若repeat 0单次定时器只执行停止操作返回0注意它内部等价于stop 后用(repeat, repeat)重新 start因此重启后原本的 timeout 被丢弃。测试 test/test-timer-again.c 展示了它在回调中续期另一个定时器的用法是看门狗/心跳模式的典型范式。void uv_timer_set_repeat(uv_timer_t* handle, uint64_t repeat)设置重复间隔单位毫秒。定时器将按给定间隔调度运行无论回调执行耗时多少在时间片超限time-slice overrun的情况下遵循正常的定时器语义即不补跑、尽快执行。源码实现非常简单src/timer.c仅仅写入handle-repeat字段。因此要注意文档强调的坑若在定时器回调内部调用uv_timer_set_repeat新的 repeat 值不会立即生效。若定时器原本是单次的它已经被停止不会自动变为重复若原本是重复的则旧值已被用来调度下一次触发。正确做法是结合uv_timer_again使用先set_repeat再uv_timer_again让新间隔立即生效。uint64_t uv_timer_get_repeat(const uv_timer_t* handle)获取定时器的 repeat 值毫秒。注意它返回的是配置值而非剩余时间与uv_timer_get_due_in互补。测试 test/test-timer-again.c 用uv_timer_get_repeat断言了句柄的重复间隔。uint64_t uv_timer_get_due_in(const uv_timer_t* handle)获取定时器距离到期的剩余时间相对uv_now若已过期则返回0。该 API 自1.40.0版本加入include/uv.h。源码实现src/timer.cuint64_t uv_timer_get_due_in(const uv_timer_t* handle) { if (handle-loop-time handle-timeout) return 0; return handle-timeout - handle-loop-time; }由于返回的是相对loop-time的差值若想获得精确的剩余毫秒数建议先调用uv_update_time刷新循环时钟。测试 test/test-timer.c 验证了不同量级超时下的返回值含超长超时钳制后的281474976710655。底层调度二叉最小堆与事件循环集成理解 API 语义后再看定时器在 libuv 内部的实现机制能帮助你避免踩坑并做出更优的调度决策。最小堆Min-Heap管理libuv 用二叉最小堆管理所有活跃定时器src/timer.c堆序由timer_less_than定义优先比较绝对到期时刻timeout若相同则比较start_id——由loop-timer_counter在uv_timer_start时递增分配src/unix/loop.c从而保证同时到期的定时器按启动先后顺序触发FIFO 稳定性。每次uv_timer_start将节点插入堆heap_insertuv_timer_stop从堆中移除heap_remove两者均为 O(log n) 复杂度。事件循环中的执行时机定时器的执行被嵌入uv_run的主循环中src/unix/core.c。一个标准迭代UV_RUN_DEFAULT中与定时器相关的步骤是进入循环前兼容性处理更新 now 并先跑一次定时器uv__run_timers每次迭代执行 pending → idle → prepare →I/O 轮询→ check → closingI/O 轮询之后uv__update_time(loop)刷新 now随后uv__run_timers(loop)执行所有到期定时器。关键点在于定时器总是在每轮 I/O 轮询之后集中执行而不是被操作系统实时唤醒后立刻执行。uv__backend_timeoutsrc/unix/core.c通过uv__next_timeoutsrc/timer.c取出堆顶定时器的剩余时间作为 I/O 轮询的阻塞上限——这保证事件循环能按时醒来处理到期的定时器若堆为空则返回 -1无限阻塞。uv__next_timeout还会把差值钳制在INT_MAX以内避免平台 poll 超时参数溢出。uv__run_timers的执行细节void uv__run_timers(uv_loop_t* loop) { /* 1. 把堆顶所有已到期的定时器摘出移入 ready_queue */ for (;;) { heap_node heap_min(timer_heap(loop)); if (heap_node NULL || handle-timeout loop-time) break; uv_timer_stop(handle); uv__queue_insert_tail(ready_queue, handle-node.queue); } /* 2. 依序先 uv_timer_again 重排再调用用户回调 */ while (!uv__queue_empty(ready_queue)) { ... uv_timer_again(handle); handle-timer_cb(handle); } }这段代码src/timer.c同时解释了文档中的多个语义重复定时器在回调前被重排uv_timer_again用repeat作为新 timeout 重新入堆因此下次到期时刻 当前循环 now repeat与回调耗时无关——这正是50 ms 间隔 17 ms 回调 33 ms 后再触发的来源不堆叠回调到期时刻到达后才执行一次若被其他任务阻塞回调尽快执行但绝不补跑执行顺序稳定ready_queue保持堆序摘出顺序同到期时刻的按start_id排序。关闭与清理uv_timer_close实现为直接调用uv_timer_stopsrc/timer.c即关闭句柄时自动从调度结构中移除无需手动先 stop 再 close。完整使用示例综合以上 API一个覆盖单次、重复、回调内改期三种场景的最小示例#include stdio.h #include uv.h static uv_timer_t once_timer; static uv_timer_t repeat_timer; static void once_cb(uv_timer_t* handle) { printf(单次定时器触发\n); uv_close((uv_handle_t*)handle, NULL); /* 用完即关 */ } static void repeat_cb(uv_timer_t* handle) { static int n 0; printf(重复定时器第 %d 次触发剩余 %llu ms\n, n, (unsigned long long) uv_timer_get_due_in(handle)); if (n 5) { uv_timer_set_repeat(handle, 200); /* 回调内改间隔需配合 again */ uv_timer_again(handle); /* 让新间隔立即生效 */ } if (n 10) uv_close((uv_handle_t*)handle, NULL); } int main(void) { uv_loop_t* loop uv_default_loop(); uv_timer_init(loop, once_timer); uv_timer_start(once_timer, once_cb, 100, 0); /* 100ms 后触发一次 */ uv_timer_init(loop, repeat_timer); uv_timer_start(repeat_timer, repeat_cb, 0, 50); /* 下个迭代首次触发随后每 50ms 一次 */ return uv_run(loop, UV_RUN_DEFAULT); }要点回顾timeout0表示最早在下一次循环迭代触发单次定时器用repeat0触发后不会自动重跑回调内修改重复间隔必须uv_timer_set_repeatuv_timer_again组合使用否则新值不生效定时器是活跃句柄会让uv_run保持运行因此终止任务时要uv_close关闭句柄。相关测试与进一步阅读仓库中的测试是理解定时器语义的最佳佐证推荐按需阅读test/test-timer.c单次/两次/重复触发、零超时、超长超时与uv_timer_get_due_in边界test/test-timer-again.c回调内用uv_timer_again续期其他定时器test/test-timer-from-check.c定时器与 prepare/check 句柄在同一迭代中的执行顺序定时器晚于 checktest/test-timer-again.c 与 [test/test-timer-huge-timeout.c]对应实现见 test/test-timer.c覆盖了uv_timer_again与巨量超时的回归场景。定时器句柄与事件循环其他部分的关系可继续阅读 docs/src/loop.rst循环运行模型与uv_update_time、docs/src/handle.rst句柄公共 API 与生命周期以及 docs/src/prepare.rstprepare/check 阶段的对比。若想了解uv__next_timeout如何驱动 I/O 轮询的阻塞时长可直接阅读 src/unix/core.c。【免费下载链接】libuvCross-platform asynchronous I/O项目地址: https://gitcode.com/gh_mirrors/li/libuv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表