swoole 4.4+ 默认采用分层时间轮(5级),非最小堆,增删为o(1),支持毫秒精度;id可复用,回调中不可调协程函数,需避免阻塞以防止漂移。

定时器用的是最小堆还是时间轮
Swoole 4.4+ 的底层定时器默认采用时间轮(Timing Wheel),不是最小堆。最小堆在大量定时器场景下每次插入/删除都是 O(log n),而时间轮的增删操作是 O(1),更适合高并发短周期任务(比如毫秒级心跳、连接空闲检测)。
但注意:Swoole 并非纯单层时间轮,而是分层时间轮(hierarchical timing wheel),包含 5 级轮子(毫秒、秒、分钟、小时、天),每级容量固定,通过溢出机制逐级传递。这能兼顾精度和内存占用。
实操建议:
- 若你调用
swoole_timer_tick()或swoole_timer_after(),底层自动走时间轮路径,无需干预 - 想验证当前是否启用时间轮?可查看源码
src/reactor/timer.h中struct _swTimer的use_timing_wheel字段,默认为 1 - 编译时禁用时间轮(仅用于调试对比)需加
--disable-timing-wheel,此时回退到最小堆实现,但不推荐生产使用
为什么 timer_add 返回的 ID 会重复
返回的定时器 ID 是从 1 开始的自增整数,但并非永久唯一——它本质是 timer->id 在内部数组中的索引取模复用。当定时器被触发或手动删除后,其槽位会被回收,后续新定时器可能拿到相同 ID。
常见错误现象:swoole_timer_clear($id) 失败,或清除到别的定时器,往往是因为 ID 已被复用。
实操建议:
- 绝不依赖 ID 的全局唯一性做业务逻辑(比如存 DB 当主键、传给前端轮询)
- 需要长期跟踪某个定时器?改用持有其返回的
$timer_id变量,并在回调中置 null 或打标记,而非靠 ID 查表 - 调试时可用
swoole_timer_info($id)检查该 ID 当前是否有效、是否已触发,返回 false 表示已销毁或不存在
定时器回调里调用协程函数会 crash 吗
会,且大概率 segfault。因为 Swoole 定时器底层运行在 reactor 线程(非协程上下文),而 co::sleep()、mysql->query() 等协程函数必须在协程内执行。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
典型错误场景:在 swoole_timer_tick() 回调里直接写 co::sleep(0.1),或试图 new Swoole\Coroutine\Http\Client()。
实操建议:
- 定时器回调必须是同步、无阻塞、非协程安全的代码;所有 I/O 操作应转交 task 进程或投递到协程调度器
- 正确做法是用
swoole_async_task()或go(function () { ... })启动新协程(注意:go 必须在协程环境里调用,定时器回调不是协程环境,所以得先判断或换用 event loop 投递) - 更稳妥的方式是改用
Swoole\Timer::tick()(PHP 层封装),它内部已做协程环境适配,但注意:它仍运行在 reactor 线程,只是对协程函数做了保护性拦截,实际调用仍需你自己规避
如何让定时器精度达到毫秒级且不漂移
Swoole 时间轮默认支持毫秒级精度(最小 tick 为 1ms),但“不漂移”取决于系统时钟、CPU 负载和 reactor 线程是否被长时间阻塞。
关键限制:reactor 线程一旦执行耗时 >1ms 的回调(如密集计算、同步文件读写),后续所有定时器都会顺延,出现累积延迟。
实操建议:
- 严格禁止在定时器回调中做任何同步阻塞操作;用
strace -e trace=epoll_wait,read,write观察 reactor 是否卡住 - 高频定时器(如 10ms 心跳)建议合并逻辑,减少回调次数;或改用
SWOOLE_HOOK_ALL+ 协程 sleep 循环(适用于 worker 进程内独立控制) - 线上排查漂移:开启
SWOOLE_LOG_DEBUG,搜索日志中timer: fire late by xxx ms,定位具体哪个回调拖慢了 reactor
时间轮本身很稳,真正不稳的是你写的回调代码——这点最容易被忽略。










