Skip to main content

什么是并发处理

对多个外部 API 的请求、数据库的聚合等相互不依赖的处理,如果顺序执行,总耗时会等于每个任务耗时的累加。 如果把它们同时执行,那么总耗时就会缩短到最慢那一项的耗时。 Laravel 的 Concurrency Facade 通过简洁的 API 实现了这种并行执行。
Concurrency Facade 在 Laravel 11 中引入,Laravel 13 中仍然可用。 默认驱动使用子 PHP 进程,因此无需额外的扩展包即可运行。

工作原理

Concurrency Facade 会将传入的闭包序列化后发送给一个隐藏的 Artisan 命令,并让它们各自作为独立的 PHP 进程执行。 处理完成后,会将返回值序列化后返回给父进程。 它提供了三种驱动。

基本用法

Concurrency::run()

run() 方法传入闭包数组,这些闭包会被并行执行。 返回值是各闭包返回值组成的数组。
可以通过数组解构把每个结果分别赋值给变量。 闭包的顺序与返回值顺序一致。

指定驱动

如果希望使用特定的驱动,可以通过 driver() 方法指定。
要修改默认驱动,可以先发布配置文件,然后更新 default 选项。

fork 驱动的用法

fork 驱动比 process 驱动更快,但只能在 PHP 的 CLI 环境(Artisan 命令或队列 worker)中使用。 Web 请求中无法使用。 使用前请先安装 spatie/fork 扩展包。
fork 驱动在 Web 请求中无法工作。请在 Artisan 命令或队列 worker 中进行并发处理时使用它。

不接收执行结果:Concurrency::defer()

如果你并不关心处理结果,希望在返回 HTTP 响应之后再在后台执行,可以使用 defer() 方法。
调用 defer() 时闭包不会立即执行。 它们会在 HTTP 响应发送给用户之后并行执行。
埋点数据记录、缓存预热等不需要让用户等待的处理,非常适合使用 defer()

实战示例:同时调用多个外部 API

以电商网站的仪表盘为例,需要从库存管理 API、销售统计 API、物流状态 API 三个接口获取信息。

顺序执行(改造前)

并行执行(改造后)

如果每个 API 各需要 1 秒,顺序执行总共要 3 秒,而并行执行大约 1 秒即可完成。

同时执行多条数据库聚合

process 驱动执行数据库聚合时,每个子进程都会新建一个数据库连接。 并发数较高时请注意数据库的最大连接数。

Context 的自动传播

在 Laravel 13.31(#61419)中,process 驱动会自动把当前的 Context 传播到子进程。 在此之前,通过 Concurrency::run()Concurrency::defer() 启动的子进程无法继承 Context::add() 设置的值,日志中输出的 context 会在父进程与子进程之间出现差异。 修复后,内部使用与队列任务传递上下文相同的机制。
  1. 启动子进程之前,将 Context::dehydrate() 的结果序列化为 JSON,作为 __LARAVEL_CONTEXT 环境变量传入
  2. 子进程(隐藏的 Artisan 命令 invoke-serialized-closure)启动后读取 __LARAVEL_CONTEXT,并对 Context 仓库执行 hydrate()
因此,可以在闭包内直接引用在闭包外部设置的 Context。
fork 驱动使用 spatie/fork 提供的操作系统级 fork(),子进程会直接复制父进程的内存空间来启动。 因此 Context 本来就自然共享,这次的修复主要针对 process 驱动(另起进程的方式)。
只要把请求 ID、用户 ID 等用于日志关联的值放入 Context,就能以相同的键跟踪 Concurrency::run() 并发执行的处理产生的日志。 详细用法请参考共享上下文

测试环境的配置

在测试环境中可以使用 sync 驱动,让闭包顺序执行。 省去了并行进程的启动开销,测试可以更快完成。
或者在 .env.testing 中修改默认驱动。

注意事项

闭包会被序列化传给子进程。 不能从闭包外部捕获无法序列化的对象(数据库连接、文件句柄、资源等)。 需要的对象请在闭包内部重新创建。
process 驱动需要启动子进程,因此如果处理本身非常短(几毫秒以内),并行执行未必更快。 HTTP 请求、数据库聚合等耗时 100ms 以上的处理,通过并行化才能真正体现出效果。
fork 驱动会 fork PHP 进程,因此无法在 Web 请求(FPM 或 Apache)中使用。 请只在 Artisan 命令或队列 worker 中使用它。
并行执行中若发生异常,run() 会将异常重新抛出。 如果希望某个任务出错时其他任务仍能继续执行,请在闭包内使用 try/catch

下一步

队列与任务

了解如何使用队列与任务在后台异步执行处理
最后修改于 2026年9月11日