有时您可能需要执行几个相互独立的慢任务。在许多情况下,通过并发执行任务可以实现显著的性能提升。Laravel 的 Concurrency 门面提供了一个简单、方便的 API,用于并发执行闭包。
工作原理
Laravel 通过序列化给定的闭包并将其分派到一个隐藏的 Artisan CLI 命令来实现并发,该命令在其自己的 PHP 进程中反序列化闭包并调用它。在闭包被调用后,结果值会被序列化回父进程。
Concurrency 门面支持三种驱动程序:process(默认)、fork 和 sync。
fork 驱动程序相比于默认的 process 驱动程序提供了更好的性能,但只能在 PHP 的 CLI 上下文中使用,因为 PHP 不支持在 Web 请求期间进行分叉。在使用 fork 驱动程序之前,您需要安装 spatie/fork 包:
composer require spatie/fork
sync 驱动程序主要在测试期间有用,当您希望禁用所有并发并简单地在父进程中顺序执行给定的闭包时。
功能尝试
测试代码
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Concurrency;
class SendEmails extends Command
{
protected $signature = 'app:send-emails';
public function handle()
{
// 切换 子进程启动 fork process
[$a, $b] = Concurrency::driver('fork')
->run([
fn () => $this->test(5),
fn () => $this->test(10),
]);
dd($a, $b);
}
private function test(int $a): int
{
sleep($a);
return $a;
}
}
执行结果
// process 方式
root@653d4ba60192:/var/www# ps -axjf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 33 33 33 pts/1 749 Ss 0 0:00 bash
33 749 749 33 pts/1 749 S+ 0 0:00 \_ php artisan app:send-emails
749 752 749 33 pts/1 749 S+ 0 0:00 \_ sh -c '/usr/bin/php8.3' 'artisan' invoke-serialized-clo
752 754 749 33 pts/1 749 S+ 0 0:00 | \_ /usr/bin/php8.3 artisan invoke-serialized-closure
749 753 749 33 pts/1 749 S+ 0 0:00 \_ sh -c '/usr/bin/php8.3' 'artisan' invoke-serialized-clo
753 755 749 33 pts/1 749 S+ 0 0:00 \_ /usr/bin/php8.3 artisan invoke-serialized-closure
// fork 方式
root@653d4ba60192:/var/www# ps -axjf
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 402 402 402 pts/2 788 Ss 0 0:00 bash
402 788 788 402 pts/2 788 R+ 0 0:00 \_ ps -axjf
0 33 33 33 pts/1 779 Ss 0 0:00 bash
33 779 779 33 pts/1 779 S+ 0 0:00 \_ php artisan app:send-emails
779 781 779 33 pts/1 779 S+ 0 0:00 \_ php artisan app:send-emails
779 782 779 33 pts/1 779 S+ 0 0:00 \_ php artisan app:send-emails
process与fork启动方式比较
两种方式都会等待所有的子方法都执行完成后输出结果并结束脚本,只是启动子进程的方式不同。
一、启动方式的本质区别
-
第一种情况(通过
sh -c间接启动)- 特征:父进程
php artisan app:send-emails(PID 749)通过sh -c命令启动子进程(如 PID 752、753)。 - 原理:
sh -c会创建一个新的 Shell 进程来执行后续命令(如/usr/bin/php8.3),因此子进程的父进程是 Shell(PID 752/753),而非直接继承自原 PHP 进程。 - 典型场景:通过
system()、exec()等函数调用外部命令时,若未绕过 Shell,则会生成中间 Shell 进程。
- 特征:父进程
-
第二种情况(直接由 PHP 进程启动)
- 特征:父进程
php artisan app:send-emails(PID 779)直接创建子进程(PID 781、782),没有中间 Shell 进程。 - 原理:使用
fork()+exec()直接加载 PHP 可执行文件,子进程与原 PHP 进程形成严格的父子关系。 - 典型场景:通过编程语言的进程管理接口(如 PHP 的
pcntl_fork())直接创建子进程,或调用可执行文件时绕过 Shell。
- 特征:父进程
二、进程树结构的差异
| 特征 | 第一种情况 | 第二种情况 |
|---|---|---|
| 父进程关系 | 子进程的父进程是 Shell(PID 752/753) | 子进程的父进程是原 PHP 进程(PID 779) |
| 进程层级 | 多层级(PHP → Shell → PHP) | 单层级(PHP → PHP) |
| 资源消耗 | 更高(额外创建 Shell 进程) | 更低(无中间进程) |
| 信号传递 | 需通过 Shell 中转,可能延迟或丢失 | 直接由父进程管理,更可靠 |
| 环境变量继承 | 可能受 Shell 环境变量影响 | 直接继承父进程环境变量 |
三、技术实现对比
-
Shell 介入的影响
- 第一种方式依赖 Shell 解释命令,可能引入环境变量解析、通配符扩展等副作用。
- 第二种方式完全由程序控制,避免 Shell 的不可预测行为,安全性更高。
-
进程组与会话管理
- 在第一种情况中,子进程的 PGID(进程组 ID)与父进程不同(如 PID 749 的 PGID 是 749,而子进程 PID 752 的 PGID 是 749),但会话(SID)保持一致,说明它们属于同一会话但可能跨进程组。
- 第二种情况中,所有子进程的 PGID 和 SID 均与父进程一致(如 PID 779、781、782 的 PGID 和 SID 均为 779 和 33),表明它们属于同一进程组和会话,便于统一管理(如发送信号到整个组)。
-
调试复杂度
- 第一种方式因存在中间 Shell 进程,调试时需追踪多级调用链(如通过
pstree或ps -ejH查看层级)。 - 第二种方式结构清晰,可直接通过
gdb附加到父进程分析子进程行为。
- 第一种方式因存在中间 Shell 进程,调试时需追踪多级调用链(如通过
-
适用场景
- 间接启动:适合需要动态生成命令(如拼接参数)或依赖 Shell 功能的场景。
- 直接启动:适合高性能、高稳定性的后台任务(如守护进程或并发 Worker)。
总结
- process 驱动
适用 Web/命令行环境,兼容性好但性能中等 - fork 驱动
CLI 专用,高性能,需安装扩展包 - sync 驱动
用于测试场景的同步模式
厉害了我的然