Webman 中毫秒级定时任务需启用 Swoole/Swow 驱动、隔离单进程、使用 Timer::tick() 或 Timer::after()(浮点秒)、禁用同步 I/O 并加 try-catch,否则精度无法保障。

Webman 中用 Timer 做毫秒级任务,不是加个数字小数点就能准——默认根本跑不到毫秒精度,必须改驱动、换接口、隔离进程,否则 Timer::add(0.5, ...) 实际延迟可能飘到 120ms 甚至卡死。
为什么 Timer::add(0.1, ...) 依然不准
Workerman 默认事件循环用的是 Select 或 Event 驱动,底层靠 usleep() 轮询,最小调度间隔约 100ms。即使你传 0.1(即 100ms),也常被四舍五入或合并触发;传更小值如 0.05(50ms)基本无效,会被静默拉回 100ms 级别。
- 检查当前驱动:
var_dump(Workerman\Events\EventInterface::class),若输出Workerman\Events\Select或Workerman\Events\Event,说明没启用高精度驱动 - Swoole v5.0+ 或 Swow v1.4+ 才支持 sub-millisecond 调度,Fiber 驱动在高并发下会漂移,必须禁用
-
Timer::add()是旧式接口,在协程驱动下仍可能降级为低精度;Timer::tick()和Timer::after()才是协程原生推荐接口
协程 Timer::tick 必须配合 Swoole/Swow 驱动
只有在明确启用了 Swoole 或 Swow 事件循环的前提下,Timer::tick() 才能真正达到毫秒级稳定触发。它不依赖轮询,而是基于内核事件通知,实测可稳定维持 ±0.3ms 偏差。
- 在
config/process.php中为目标进程显式指定驱动:'eventLoop' => Workerman\Events\Swoole::class - 在进程文件(如
app/process/MicroTask.php)的onWorkerStart中引入:use Workerman\Coroutine\Timer; - 注册任务时用浮点数毫秒值:
Timer::tick(50, fn() => echo "50ms tick\n");(注意单位是毫秒,不是秒) - 避免混用:
Timer::add()在协程进程里可能被兼容层拦截,导致精度丢失,一律换成Timer::tick()或Timer::after()
单进程隔离 + try-catch 是硬性要求
毫秒级任务哪怕单次执行耗时 20ms,放在共享 Worker 进程里也会拖垮整个事件循环——其他定时器、HTTP 请求、WebSocket 心跳全跟着抖动。这不是“优化问题”,是架构前提。
立即学习“PHP免费学习笔记(深入)”;
- 必须单独起一个进程运行毫秒任务,且
count => 1,禁止多实例竞争同一事件循环 - 所有回调必须包裹
try/catch,否则未捕获异常会让整个Crontab或Timer实例静默退出,无日志、无告警 - 禁用同步 I/O:不能调
file_get_contents()、sleep()、阻塞 Redis 命令;改用Co\Http\Client、Co\Redis等协程客户端 - 时间校准建议用
Co::gettimeofday(true)获取单调浮点秒,而非microtime(true)(后者可能因系统时钟调整跳变)
Timer::after 的延迟值必须是浮点数
Timer::after() 用于单次延迟任务(比如 120.5ms 后触发),但它的参数类型敏感:整数会被当“秒”处理,浮点数才按“秒”解析——这和直觉相反,也是最容易踩的坑。
- 错误写法:
Timer::after(120, $fn)→ 等待 120 秒,不是 120ms - 正确写法:
Timer::after(0.120, $fn)→ 等待 120 毫秒 - 如果需要动态计算延迟,务必确保结果是浮点类型:
(float) $ms / 1000,而不是$ms / 1000(PHP 整数除法可能截断) - 该接口不支持毫秒整数参数,没有
Timer::afterMs(120, ...)这种方法,别找
毫秒级调度真正的门槛不在代码行数,而在驱动选择、进程拓扑和错误防御这三层。漏掉任意一层,Timer::tick(10, ...) 就只是个心理安慰。



















