workerman4中定时器需限定单进程执行,推荐在onworkerstart中通过$worker->id===0判断仅由首个子进程注册;需防重入并清理定时器,避免文件锁或外部cron等伪解决方案。

Workerman4 中定时器在子进程重复创建,本质是每个 Worker 子进程都会独立执行 Timer::add(),导致多个进程同时跑同一个定时任务——这不是“定时器失效”,而是设计使然。关键不在删定时器,而在控制「只让一个子进程注册并运行」。
明确定时任务的执行主体
Workerman 启动后,主进程 fork 出多个子进程(由 $worker->count 指定)。所有子进程都会加载并执行你的业务代码,包括 Timer::add() 调用。结果就是:10 个子进程 → 10 个相同定时器 → 每次触发都执行 10 次。
- 定时器属于进程内资源,无法跨进程共享或同步
-
onWorkerStart回调在每个子进程中各执行一次,切勿在此处无条件加定时器 - 真正需要“全局唯一执行”的任务(如清理缓存、拉取配置、发汇总报表),必须做单点控制
推荐方案:用主进程 or 特定子进程承载定时任务
最轻量、最可靠的做法是限定仅在一个子进程中注册定时器。常用两种方式:
-
按进程 ID 判断(推荐):在
onWorkerStart中检查Worker::$pidMap或直接比对当前进程 ID 是否为某个固定序号 - 单独启一个专用 Worker:定义一个 count=1 的 Worker,专用于跑定时任务,与其他业务进程完全隔离
示例(ID 判定法):
use Workerman\Worker;
use Workerman\Lib\Timer;
$task_worker = new Worker();
$task_worker->count = 2; // 假设开 2 个子进程
$task_worker->onWorkerStart = function($worker) {
// 只让第一个子进程(索引 0)注册定时器
if ($worker->id === 0) {
Timer::add(30, function() {
echo "【定时任务】每30秒执行一次,仅由 worker-0 执行\n";
});
}
};
进阶:避免回调重入 + 安全退出
即使限定了单进程,若回调耗时超过间隔(比如 5 秒任务设成 3 秒执行),仍可能堆积或并发执行。需主动防重入:
- 用静态变量或
Atomic标记“正在运行中”,进入前判断,执行完再释放 - 在
onWorkerStop中调用Timer::del($timer_id)清理,防止进程退出时残留 - 不依赖 sleep 或阻塞 IO;优先用异步 client(如 AsyncTcpConnection)或 defer 机制
不建议的“伪解决”方式
以下做法看似省事,实则隐患明显:
- 在定时器回调里加文件锁 / Redis 分布式锁 → 增加 I/O 开销,且锁失败未处理易静默丢任务
- 用 Cron 调外部脚本再通知 Workerman → 破坏事件循环一致性,增加部署复杂度
- 在
onWorkerStart外层直接写Timer::add→ 仍会被每个子进程执行,无效











