> ## 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 個請求同時執行（總耗時約為最慢 1 個 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="閉包的限制">
    閉包會被序列化後傳給子程序。
    無法從閉包外部捕獲不可序列化的物件（資料庫連線、檔案 handle、resource 等）。
    需要的物件請在閉包內另行建立。

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

    // 可以：在閉包中取得資料
    $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-TW/queues">
  學習如何將處理透過佇列與工作，非同步地在背景執行
</Card>


## Related topics

- [集合](/zh-TW/collections.md)
- [密碼重設](/zh-TW/passwords.md)
- [Process](/zh-TW/processes.md)
- [Fleet Mode](/zh-TW/packages/laravel-copilot-sdk/fleet-mode.md)
- [中介軟體](/zh-TW/middleware.md)
