> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 并发处理

> 介绍如何使用 Laravel 13 的 Concurrency Facade 同时执行多个任务，从而提升应用的性能。

## 什么是并发处理

对多个外部 API 的请求、数据库的聚合等相互不依赖的处理，如果**顺序执行**，总耗时会等于每个任务耗时的累加。
如果把它们**同时执行**，那么总耗时就会缩短到最慢那一项的耗时。

Laravel 的 `Concurrency` Facade 通过简洁的 API 实现了这种并行执行。

<Info>
  `Concurrency` Facade 在 Laravel 11 中引入，Laravel 13 中仍然可用。
  默认驱动使用子 PHP 进程，因此无需额外的扩展包即可运行。
</Info>

## 工作原理

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

它提供了三种驱动。

| 驱动        | 说明                                                  |
| --------- | --------------------------------------------------- |
| `process` | 默认驱动。启动子 PHP 进程执行，可在 Web 请求中运行                      |
| `fork`    | 需要 `spatie/fork` 扩展包。通过 fork 进程运行，仅可在 CLI 中使用，但速度更快 |
| `sync`    | 不并行执行，顺序运行。适合测试场景                                   |

## 基本用法

### Concurrency::run()

向 `run()` 方法传入闭包数组，这些闭包会被并行执行。
返回值是各闭包返回值组成的数组。

```php theme={null}
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;

[$userCount, $orderCount] = Concurrency::run([
    fn () => DB::table('users')->count(),
    fn () => DB::table('orders')->count(),
]);
```

可以通过数组解构把每个结果分别赋值给变量。
闭包的顺序与返回值顺序一致。

### 指定驱动

如果希望使用特定的驱动，可以通过 `driver()` 方法指定。

```php theme={null}
$results = Concurrency::driver('fork')->run([
    fn () => fetchFromApiA(),
    fn () => fetchFromApiB(),
]);
```

要修改默认驱动，可以先发布配置文件，然后更新 `default` 选项。

```shell theme={null}
php artisan config:publish concurrency
```

## fork 驱动的用法

`fork` 驱动比 `process` 驱动更快，但只能在 PHP 的 CLI 环境（Artisan 命令或队列 worker）中使用。
Web 请求中无法使用。

使用前请先安装 `spatie/fork` 扩展包。

```shell theme={null}
composer require spatie/fork
```

<Warning>
  `fork` 驱动在 Web 请求中无法工作。请在 Artisan 命令或队列 worker 中进行并发处理时使用它。
</Warning>

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

如果你并不关心处理结果，希望在返回 HTTP 响应之后再在后台执行，可以使用 `defer()` 方法。

```php theme={null}
use App\Services\Metrics;
use Illuminate\Support\Facades\Concurrency;

Concurrency::defer([
    fn () => Metrics::report('users'),
    fn () => Metrics::report('orders'),
]);
```

调用 `defer()` 时闭包不会立即执行。
它们会在 HTTP 响应发送给用户之后并行执行。

<Tip>
  埋点数据记录、缓存预热等不需要让用户等待的处理，非常适合使用 `defer()`。
</Tip>

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

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

### 顺序执行（改造前）

```php theme={null}
use Illuminate\Support\Facades\Http;

public function dashboard(): array
{
    // 每次请求都要等待前一个完成（合计大约 3 秒）
    $inventory = Http::get('https://api.example.com/inventory')->json();
    $sales     = Http::get('https://api.example.com/sales')->json();
    $shipping  = Http::get('https://api.example.com/shipping')->json();

    return compact('inventory', 'sales', 'shipping');
}
```

### 并行执行（改造后）

```php theme={null}
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\Http;

public function dashboard(): array
{
    // 3 个请求同时执行（总耗时约等于最慢的那个 API）
    [$inventory, $sales, $shipping] = Concurrency::run([
        fn () => Http::get('https://api.example.com/inventory')->json(),
        fn () => Http::get('https://api.example.com/sales')->json(),
        fn () => Http::get('https://api.example.com/shipping')->json(),
    ]);

    return compact('inventory', 'sales', 'shipping');
}
```

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

### 同时执行多条数据库聚合

```php theme={null}
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;

public function statistics(): array
{
    [$totalUsers, $activeUsers, $totalOrders, $revenue] = Concurrency::run([
        fn () => DB::table('users')->count(),
        fn () => DB::table('users')->where('active', true)->count(),
        fn () => DB::table('orders')->count(),
        fn () => DB::table('orders')->sum('total_amount'),
    ]);

    return [
        'total_users'  => $totalUsers,
        'active_users' => $activeUsers,
        'total_orders' => $totalOrders,
        'revenue'      => $revenue,
    ];
}
```

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

## 测试环境的配置

在测试环境中可以使用 `sync` 驱动，让闭包顺序执行。
省去了并行进程的启动开销，测试可以更快完成。

```php theme={null}
// tests/Feature/DashboardTest.php
use Illuminate\Support\Facades\Concurrency;

public function test_dashboard_returns_correct_data(): void
{
    Concurrency::fake(); // 切换到 sync 驱动

    $response = $this->get('/dashboard');

    $response->assertStatus(200);
}
```

或者在 `.env.testing` 中修改默认驱动。

```ini theme={null}
CONCURRENCY_DRIVER=sync
```

## 注意事项

<AccordionGroup>
  <Accordion title="闭包的限制">
    闭包会被序列化传给子进程。
    不能从闭包外部捕获无法序列化的对象（数据库连接、文件句柄、资源等）。
    需要的对象请在闭包内部重新创建。

    ```php theme={null}
    // NG：从外部捕获 Eloquent 模型
    $user = User::find(1);
    Concurrency::run([
        fn () => $user->orders()->count(), // 可能无法序列化
    ]);

    // OK：在闭包内部获取数据
    $userId = 1;
    Concurrency::run([
        fn () => Order::where('user_id', $userId)->count(),
    ]);
    ```
  </Accordion>

  <Accordion title="process 驱动的开销">
    `process` 驱动需要启动子进程，因此如果处理本身非常短（几毫秒以内），并行执行未必更快。
    HTTP 请求、数据库聚合等耗时 100ms 以上的处理，通过并行化才能真正体现出效果。
  </Accordion>

  <Accordion title="fork 驱动的限制">
    `fork` 驱动会 fork PHP 进程，因此无法在 Web 请求（FPM 或 Apache）中使用。
    请只在 Artisan 命令或队列 worker 中使用它。
  </Accordion>

  <Accordion title="异常的处理">
    并行执行中若发生异常，`run()` 会将异常重新抛出。
    如果希望某个任务出错时其他任务仍能继续执行，请在闭包内使用 `try/catch`。

    ```php theme={null}
    Concurrency::run([
        function () {
            try {
                return Http::get('https://api.example.com/data')->json();
            } catch (\Exception $e) {
                return null; // 失败时返回 null
            }
        },
    ]);
    ```
  </Accordion>
</AccordionGroup>

## 下一步

<Card title="队列与任务" icon="layer-group" href="/zh/queues">
  了解如何使用队列与任务在后台异步执行处理
</Card>


## Related topics

- [进程](/zh/processes.md)
- [Laravel Reverb](/zh/reverb.md)
- [Cloud Sessions](/zh/packages/laravel-copilot-sdk/cloud-sessions.md)
- [Laravel Octane](/zh/octane.md)
- [HTTP 客户端](/zh/http-client.md)
