协程定时器必须在协程内创建,主进程或非协程上下文中调用Swoole\Timer::tick()或after()会失败且无明确提示;需用go()等进入协程后调用,tick需手动clear,after虽自动销毁但协程被kill时回调可能报错,应加exists判断;不可替代sleep,须配合Channel实现超时控制;其执行依赖事件循环,CPU密集操作会阻塞回调触发。

协程定时器必须在协程内创建
直接在主进程或非协程上下文中调用 Swoole\Timer::tick() 或 Swoole\Timer::after() 会失败,返回 false,且无明确错误提示。这是因为 Swoole 的协程定时器底层依赖当前协程的调度上下文,主进程没有协程 ID,无法绑定回调执行环境。
常见错误现象:代码看似执行成功(返回 int 类型 timer_id),但回调函数从不触发;或者触发时出现 Fatal error: Uncaught RuntimeException: Coroutine is not running。
- 正确做法是确保调用前已进入协程,例如用
Swoole\Coroutine::create()、go(),或在协程 HTTP/WS 回调中使用 -
go(function () { Swoole\Timer::tick(1000, fn() => var_dump('tick')); });是安全的 - 若在
Swoole\Http\Server的onRequest中使用,该回调本身已在协程中,可直接调用
tick 和 after 的行为差异直接影响资源释放
Swoole\Timer::tick() 创建的是周期性定时器,Swoole\Timer::after() 是单次定时器,但二者在协程环境下生命周期管理逻辑不同:前者需手动 Swoole\Timer::clear(),后者到期自动销毁。
容易踩的坑是忘记清理 tick 定时器,尤其在短生命周期协程(如一次 HTTP 请求)中反复创建,导致 timer_id 泄漏、内存缓慢增长,最终可能触发 PHP Warning: Swoole\Timer::tick(): too many timers。
Swoole 6.1.1 是一个专为 PHP 设计的高性能事件驱动并发网络引擎。作为稳定版,它修复了编译时对 zlib 依赖的缺失及 curl 模块的内存安全风险。该版本支持协程、多线程与多进程架构,内置 TCP/HTTP/WebSocket 服务器,能够显著提升 PHP 在微服务、实时通信等场景下的执行效率与并发能力。
- 每个
tick必须配对Swoole\Timer::clear($timer_id),推荐在协程退出前统一清理(可用defer) -
after虽自动清理,但在协程被 kill(如超时)时,回调可能仍被投递到事件循环——此时回调执行时协程已不存在,会报错Coroutine does not exist - 稳妥做法:在
after回调开头加if (!Swoole\Coroutine::exists(Swoole\Coroutine::cid())) return;
协程定时器不能替代 sleep,但可组合实现“等待+超时”
协程中禁止用 sleep() 或 usleep(),它们会阻塞整个进程;而 Swoole\Coroutine::sleep() 是协程友好的,但它只是挂起当前协程,并不提供“到期通知”能力。如果需要“等待某条件达成,最长等 5 秒”,单纯 sleep 不够,得靠定时器配合。
典型场景:调用第三方 API,希望 3 秒后强制中断并返回默认值。
- 不要写
sleep(3); // 错误!阻塞 - 正确方式:启动
after(3000, fn() => $done = true),同时异步发起请求,在回调里检查$done状态 - 更简洁:用
Swoole\Coroutine::wait()+Channel+ 定时器发送超时信号,避免全局变量共享 - 注意:定时器回调和主协程并发执行,所有共享变量需考虑竞态,优先用
Channel或Atomic
协程定时器与 event loop 的关系决定执行时机
协程定时器不是独立线程,它依赖 Swoole 事件循环驱动。这意味着:如果当前协程长时间占用 CPU(如密集计算、死循环),即使定时器到期,回调也不会被执行,直到协程让出控制权(如 await、sleep、I/O 等)。
性能影响明显:一个协程里跑 while (true) { $i++; },再设 tick(100, ...),回调永远不会触发。
- 协程定时器的精度受事件循环调度频率影响,通常误差在毫秒级,不适合高精度计时
- 若需精确间隔(如每 100ms 执行),应避免在回调中做耗时操作,否则会累积延迟
- 调试时可通过
swoole_get_local_config()['log_level'] = 5查看 timer 相关日志,确认是否注册成功、是否到期触发
go,或少写一行 defer Swoole\Timer::clear(...),问题就藏得又深又静。

















