PHP 傳統上運行在請求-響應模式,缺乏原生的持久化定時任務能力。通過 pcntl 多進程 與 libevent 事件驅動 結合,可在 CLI 模式下構建高性能、精確的毫秒級定時器,適用於守護進程、任務調度、長連接心跳等場景。本文深度解析實現原理與完整代碼。
pcntl_fork()、信號處理、進程優先級等 Unix 進程控制原語,僅 CLI 模式可用。event_base 事件循環、定時事件、I/O 事件、信號事件,PHP 需安裝 pecl install libevent。$pid = pcntl_fork();
if ($pid === -1) {
die('fork 失敗');
} elseif ($pid === 0) {
// 子進程邏輯
echo "子進程 PID: " . getmypid() . "\n";
exit(0);
} else {
// 父進程邏輯,返回子進程 PID
echo "父進程創建子進程: $pid\n";
}
關鍵點:fork 後父子進程從調用點繼續執行,返回值區分身份(父進程得子 PID,子進程得 0)。
PHP 的信號處理基於 declare(ticks=N) 機制:每執行 N 條低級語句檢查一次信號隊列。
declare(ticks=1); // 每條語句檢查,精度高但有性能開銷
pcntl_signal(SIGTERM, function($signo) {
echo "收到 SIGTERM,優雅退出\n";
exit(0);
});
// 發送信號測試
posix_kill(getmypid(), SIGTERM);
function newChild($funcName, ...$args) {
$pid = pcntl_fork();
if ($pid === 0) {
// 子進程執行目標函數並退出
if (function_exists($funcName)) {
exit(call_user_func_array($funcName, $args));
}
exit(-1);
} elseif ($pid === -1) {
throw new RuntimeException('無法創建子進程');
}
return $pid; // 父進程返回子 PID
}
function timer($callback, $microseconds) {
$base = event_base_new();
$event = event_new();
// 設置定時事件:fd=0, EV_TIMEOUT, 回調
event_set($event, 0, EV_TIMEOUT, $callback);
event_base_set($event, $base);
event_add($event, $microseconds); // 微秒
// 進入事件循環(阻塞)
event_base_loop($base);
}
// 回調示例
function onTimer() {
echo "[" . date('H:i:s.u') . "] Timer triggered\n";
}
// 啟動一個 5 秒觸發一次的定時器子進程
$pid = newChild('timer', 'onTimer', 5_000_000); // 5秒 = 5,000,000 微秒
if ($pid > 0) {
echo "主進程退出,定時器在子進程運行 (PID: $pid)\n";
// 主進程可繼續做其他事,或等待信號
}
class TaskScheduler {
private $base;
private $tasks = []; // [taskId => [event, interval, callback, data]]
public function __construct() {
$this->base = event_base_new();
}
public function addTask(callable $callback, int $intervalMs, $data = null, string $id = null): string {
$id = $id ?? uniqid('task_');
$event = event_new();
event_set($event, 0, EV_TIMEOUT | EV_PERSIST, function($fd, $what, $arg) {
[$cb, $data] = $arg;
$cb($data);
}, [$callback, $data]);
event_base_set($event, $this->base);
event_add($event, $intervalMs * 1000); // 轉微秒
$this->tasks[$id] = [$event, $intervalMs, $callback, $data];
return $id;
}
public function removeTask(string $id): bool {
if (isset($this->tasks[$id])) {
event_del($this->tasks[$id][0]);
event_free($this->tasks[$id][0]);
unset($this->tasks[$id]);
return true;
}
return false;
}
public function run() {
event_base_loop($this->base);
}
}
// 使用示例
$scheduler = new TaskScheduler();
$scheduler->addTask(function() { echo "每秒任務\n"; }, 1000);
$scheduler->addTask(function($msg) { echo "$msg\n"; }, 5000, '每5秒任務');
$scheduler->run();
declare(ticks=1);
$shutdown = false;
pcntl_signal(SIGTERM, function() use (&$shutdown) {
$shutdown = true;
});
$scheduler = new TaskScheduler();
// ... 添加任務
while (!$shutdown) {
// 非阻塞事件循環(需 libevent 支持 EVLOOP_NONBLOCK 或自定義循環)
event_base_loop($scheduler->getBase(), EVLOOP_ONCE | EVLOOP_NONBLOCK);
usleep(10000); // 10ms 降低 CPU 佔用
}
// 清理資源
foreach ($scheduler->getTasks() as $id => $task) {
event_del($task[0]);
event_free($task[0]);
}
echo "調度器已優雅關閉\n";
pcntl_wait() 或 pcntl_waitpid() 回收子進程,否則產生僵屍進程耗盡 PID。建議在父進程設置 SIGCHLD 信號處理器自動回收。declare(ticks=1) 會顯著降低性能,僅在需要信號處理的文件頂部聲明,業務邏輯文件勿用。event_free()、unset() 大變量,考慮定期重啟進程。com_dotnet 調用系統計劃任務或改用 Swoole/Workerman 等擴展。| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| pcntl + libevent | 精確到毫秒、無額外依賴、系統級 | 代碼複雜、僅 CLI、Windows 不支持 | 高精度定時、守護進程、學習底層 |
| Swoole / Workerman | 高性能、协程、跨平台、生態豐富 | 需安裝擴展、學習成本 | 生產環境長連接、定時任務、微服務 |
| Crontab + PHP CLI | 簡單、穩定、系統級調度 | 最小粒度 1 分鐘、無狀態管理 | 低頻維護任務(備份、清理、報表) |
| Redis Keyspace Notifications / 延遲隊列 | 分布式、解耦、可視化 | 依賴 Redis、精度受限 | 分布式延遲任務、訂單超時取消 |
pcntl + libevent 為 PHP 提供了系統級的精確定時與多進程能力,是理解 PHP 進程模型、信號處理、事件驅動的最佳實踐場景。生產環境中,建議優先選擇 Swoole/Workerman 等成熟框架,將底層複雜性封裝,專注業務邏輯。掌握底層原理,能在框架選型、故障排查、性能調優時做出更明智的決策。