Workerman 4.0.20 定时器依赖 EventLoop 驱动,需启用 libevent 扩展保障精度;必须在 onWorkerStart 中注册,用 $worker->id === 0 控制单例执行;禁止阻塞操作以防 EventLoop 停摆。

要在 Workerman 4.0.20 中稳定运行高并发长连接服务,必须理解其事件循环如何驱动定时器——它不是靠 sleep 或轮询,而是由底层 I/O 多路复用机制精确触发回调,定时器精度和生命周期完全受 EventLoop 控制。
确认当前 Worker 使用的事件循环引擎
打开终端,进入项目根目录,执行:php start.php status 查看进程状态输出中的 event-loop 字段值。
若未安装 libevent 扩展或系统缺少 libevent 库,Workerman 会自动降级为 select 引擎;select 在高并发下性能明显下降,【必须确保 libevent 已正确编译并加载】,否则 Timer::add 的实际触发间隔将严重漂移。
验证扩展是否生效:运行 php -m | grep event,有输出即表示 PHP 层已加载;再检查 php --ri event 中的 version 和 libevent version 是否匹配系统安装版本。
在 onWorkerStart 中注册定时任务
定时器必须在 Worker 进程启动后注册,不能放在全局作用域或 onMasterStart 中——主进程不运行事件循环,Timer::add 在那里调用无效。
方法一:基础间隔执行
在 $worker->onWorkerStart 回调内直接调用:Timer::add(3, fn() => echo "每3秒执行\n");
方法二:带参数与持久性控制Timer::add(1.5, [$obj, 'handlePing'], ['client_id_123'], true);
第三个参数是传递给回调的数组,第四个参数决定是否重复执行;设为 false 则只触发一次。
注意:回调函数必须是可调用类型,PHP 8.1+ 禁止传字符串函数名如 'my_func',否则抛出 TypeError。
安全停止定时器的三种操作路径
第一步:保存定时器 ID$timerId = Timer::add(2, fn() => {}); → 必须立刻保存返回的整数 ID,后续停用全靠它。
第二步:按需停止单个定时器Timer::del($timerId); → 执行后该定时器立即失效,不会再次触发。
第三步:批量清理(慎用)Timer::delAll(); → 会清空当前 Worker 进程中所有已注册的定时任务,包括其他业务模块添加的,【不可逆,且无确认机制】。
避免多进程重复执行的实战写法
Workerman 默认启动多个子进程($processCount 默认为 CPU 核心数),每个进程都会独立执行 Timer::add 注册的任务。若你只想让某任务全局只跑一次,不能依赖主进程 PID 判断(主进程不跑 EventLoop),而应采用进程 ID 绑定 + 条件注册:
在 $worker->onWorkerStart 中写:if ($worker->id === 0) { Timer::add(60, fn() => doDailyBackup()); }$worker->id 是 Worker 进程编号(从 0 开始),仅第一个 Worker 进程(ID=0)会执行该定时器。
这比用文件锁或 Redis 更轻量,适用于单机部署场景;若集群部署,需改用分布式锁。
定时器精度与阻塞风险控制
Timer::add 的最小稳定精度约 0.1 秒,即使传入 0.01,实际触发间隔也会在 100–300ms 波动——这是由底层 usleep 和主循环调度频率共同决定的,不是 bug。
绝对禁止在定时器回调中执行以下操作:
• sleep(1) 或任何阻塞式 I/O(如 file_get_contents 同步请求)
• 未 try/catch 包裹的数据库查询(异常会导致整个 Worker 进程退出)
• 无限循环或长时间计算
这些操作会让当前 Worker 进程卡死,所有连接无法响应,【EventLoop 被阻塞后,onMessage、onClose 等回调全部停滞】。

















