本文目录导读:

在 Laravel 中取消长时间运行的任务(如队列、命令、循环处理),通常推荐使用“信号”(Signal)机制,而不是“标志”(比如数据库字段、缓存键、文件锁),原因如下:
为什么“信号”优于“标志”
| 方面 | 信号(Signal) | 标志(Flag,如 DB/cache) |
|---|---|---|
| 实时性 | 操作系统随时中断进程,立即响应 | 需循环轮询,有延迟(取决于查询间隔) |
| 资源开销 | 零开销,由内核处理 | 每次轮询都有 I/O 或网络请求 |
| 可靠性 | 进程退出/信号量/释放锁等,天然安全 | 可能因进程异常退出导致标志残留 |
| 适用场景 | 守护进程、CLI 命令、队列 worker | Web 请求中简单轮询控制(如进度条) |
Laravel 中如何实现信号取消?
场景:一个后台运行的 Artisan 命令
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LongRunningCommand extends Command
{
protected $signature = 'app:long-run';
protected $description = '一个可以被取消的长时间任务';
public function handle()
{
$this->line("PID: " . getmypid());
$this->line("要取消,请运行: kill -SIGTERM " . getmypid());
// 注册信号处理
$this->trap('SIGTERM', function () {
$this->line('收到 SIGTERM,准备优雅退出...');
// 此处可以清理资源、保存进度等
exit(0); // 或设置一个终止标志供循环检查
});
$this->trap('SIGINT', function () {
$this->line('收到 Ctrl+C (SIGINT),退出...');
exit(0);
});
$shouldStop = false; // 供循环检查
// 真正的循环任务
for ($i = 0; $i < 1000 && !$shouldStop; $i++) {
// 模拟工作
sleep(1);
$this->line("处理第 {$i} 项...");
// 如果在上面的 trap 中设置了 $shouldStop = true,这里就会跳出
}
$this->line("任务结束或已取消");
}
}
更优雅的方式:使用 $this->shouldStop() 方法
Laravel 的命令基类内置了对 SIGTERM 的支持,你只需要在循环中检查 $this->shouldStop() 即可:
public function handle()
{
$this->trap('SIGTERM', fn() => $this->shouldKeepRunning = false);
$this->trap('SIGINT', fn() => $this->shouldKeepRunning = false);
while ($this->shouldKeepRunning) {
// 你的业务逻辑
}
}
如果必须使用“标志”(不推荐但可行)
适用于:前台 Web 用户控制任务暂停/继续,且无法直接发送 OS 信号给进程。
// 定义标志(Redis or DB)
// 用户点击取消时,将标志设为 true
// 在循环中轮询
while (true) {
if (Cache::get('task_cancel_flag_' . $taskId)) {
break; // 取消
}
// 执行业务
sleep(2); // 轮询间隔不宜过短
}
缺点:
- 用户取消后,可能仍需等待 0~2 秒(轮询间隔)。
- 进程崩溃后标志永远为 true,需手动清理。
推荐做法总结
| 场景 | 推荐方式 |
|---|---|
| 控制台命令/队列 worker 取消 | 信号(Signal) |
| 长时间 web 请求(如导出大文件) | 定期检查 connection->aborted() |
| 多 worker 间共享取消状态(如分布式任务) | 标志(Redis/Database) + 心跳监控 |
在 Laravel Horizon / 队列中的应用
如果你使用 Horizon 运行队列,可以直接用 pcntl_signal 对 SIGTERM 进行处理,Horizon 本身在收到 SIGQUIT / SIGTERM 时会优雅停止 worker。
// 在 Horizon 的 Job 中(不推荐,较复杂) // 推荐在 Supervisor 层面控制 worker 重启/停止
首选信号,因为它是操作系统层面最直接、最高效的取消机制,仅在无法获取进程控制权(如 Web 请求中)或需要跨进程共享取消状态时,才考虑使用标志。
如果你需要示例代码或对信号处理在 Laravel 中的具体坑点(pcntl 扩展要求)感兴趣,我可以进一步说明。