Timer::tick用于周期性任务,需手动调用clear清理;Timer::after为一次性延迟任务,执行后自动释放。二者共享最小堆管理但生命周期策略不同,混用时应统一用tick+状态标记控制启停,避免嵌套after导致内存泄漏与精度下降。

Timer::tick 是周期性任务,必须手动清理
它会按毫秒间隔反复触发回调,比如每 1000 毫秒执行一次心跳上报。只要不调用 Timer::clear($timer_id),它就会一直跑下去,哪怕 Worker 进程空闲也不会自动停。
常见错误是忘记清定时器,导致内存泄漏或重复执行——尤其在协程环境里,如果在 go 启动的协程中创建了 tick,但协程退出后没清定时器,这个定时器还在原 Worker 进程里活着。
-
Timer::tick返回一个整数$timer_id,必须保存好,后续清除全靠它 - 不能在阻塞函数(如
sleep、fread)里调用tick,否则事件循环卡住,定时器时间严重漂移 - 每个 Worker 进程独立维护自己的定时器列表,
clear只影响当前进程
Timer::after 是一次性延迟任务,执行完自动释放
它只在指定毫秒后触发一次,比如 Timer::after(5000, function() { /* 关闭超时连接 */ }),5 秒后执行完就彻底消失,不需要你操心回收。
容易踩的坑是误把它当 tick 用:有人写 Timer::after(1000, function() { doSomething(); Timer::after(1000, ...); }) 来模拟轮询,这会产生大量定时器 ID,堆内存持续增长,且调度精度差(每次都要重新入堆)。
-
after的最大延迟值是86400000(24 小时),超限会返回false - 回调函数里无法直接拿到定时器 ID,所以没法在内部自清;如需动态控制,得把 ID 存到外部变量或闭包
use进去 - 它和
tick共享同一套最小堆管理,但生命周期策略完全不同:一个进堆即出,一个反复重入
混用场景下怎么避免冲突
典型需求是“3 秒后开始心跳,每 2 秒发一次,10 秒后停”。这时候不能靠嵌套 after 或硬编码 clear 时间点,而应统一用 tick + 状态标记 + 条件判断。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
更稳妥的做法是:用 Timer::after 启动,里面记录起始时间,再开一个 tick 做轮询检查,每次回调里判断是否超时,超时则 Timer::clear 自身。
- 不要在
tick回调里反复调用Timer::after,除非你明确需要错峰调度 - Worker 进程重启时,所有定时器自动失效,但如果你用了
Timer::clearAll(),要确保它只在进程退出前调用,别在业务逻辑中途乱清 - 调试时可用
Timer::list()查当前活跃定时器,配合Timer::info($timer_id)看剩余时间,比盲猜靠谱
底层共用事件循环,但堆管理逻辑不同
两者都走 Swoole 的全局最小堆定时器管理器,但 after 触发后直接从堆里移除节点,tick 则会在回调结束前把自身重新插入堆,等待下次到期。
这意味着:高频率 tick(比如 10ms)会导致堆操作频繁,CPU 占用升高;而大量短生命周期 after(比如每请求建一个 100ms 延迟)则堆节点增删频繁,但总体压力比 tick 小。
- 4.2.10 以前版本,
$msec参数上限都是86400000;新版本对after放宽了,但tick仍受限 - 协程环境下,
tick回调运行在默认协程上下文,若需切换上下文(如 await DB 查询),得显式go新协程,否则会阻塞整个 tick 调度 - 定时器精度依赖系统时钟和事件循环负载,极端情况下(如 CPU 满载)可能延迟几十毫秒,别拿它做严格实时控制
真正难处理的不是选 tick 还是 after,而是它们混在长生命周期服务里时,谁在什么时候该被清理、清理是否彻底——尤其是跨协程、跨子进程、或结合 onWorkerStart/onWorkerStop 使用时,ID 丢失和重复清理才是高频故障点。

















