什麼是並行處理
當多個彼此不相依的處理(例如向多個外部 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 套件。
不接收執行結果:Concurrency::defer()
若你並不關心處理的結果,只希望在傳回 HTTP 回應之後,於背景執行,可以使用defer() 方法。
defer() 當下,閉包並不會立即執行。
它會在 HTTP 回應送出給使用者之後,並行地執行。
實戰範例:同時呼叫多個外部 API
想像一個電商網站的儀表板,需要從庫存管理 API、業績匯總 API、配送狀態 API 三處取得資訊。依序執行(改善前)
並行執行(改善後)
同時執行多個資料庫匯總
Context 自動傳遞
Laravel 13.31(#61419)起,process 驅動會將目前的 Context 自動傳遞至子程序。
在此之前,透過 Context::add() 設定的值並不會被繼承給以 Concurrency::run() 或 Concurrency::defer() 啟動的子程序,日誌中輸出的 context 可能在父程序與子程序間不一致。
修正後,內部使用的機制與傳遞 context 給佇列 Job 相同。
- 在啟動子程序前,將
Context::dehydrate()的結果序列化為 JSON,透過__LARAVEL_CONTEXT環境變數傳遞 - 子程序(隱藏的 Artisan 指令
invoke-serialized-closure)啟動後,會讀取__LARAVEL_CONTEXT並對Contextrepository 執行hydrate()
fork 驅動使用 spatie/fork 進行 OS 層級的 fork(),子程序會直接複製父程序的記憶體空間啟動。
因此 context 本來就會自然共享,本次修正主要針對 process 驅動(啟動另一個程序的方式)。測試時的設定
測試環境中可使用sync 驅動,讓閉包依序執行。
沒有並行執行所需的程序啟動成本,能加速測試。
.env.testing 更改預設驅動。
注意事項
閉包的限制
閉包的限制
閉包會被序列化後傳給子程序。
無法從閉包外部捕獲不可序列化的物件(資料庫連線、檔案 handle、resource 等)。
需要的物件請在閉包內另行建立。
process 驅動的額外開銷
process 驅動的額外開銷
process 驅動有子程序啟動成本,因此當處理本身非常短(低於幾毫秒)時,並行執行未必更快。
對於 HTTP 請求或資料庫匯總等耗時超過 100ms 的處理,並行化才會顯現效果。fork 驅動的限制
fork 驅動的限制
fork 驅動會 fork PHP 程序,因此無法在 Web 請求(FPM 或 Apache)中使用。
僅能在 Artisan 指令或佇列 worker 中使用。例外的處理
例外的處理
若在並行執行過程中發生例外,
run() 會將例外重新拋出。
若希望即使個別處理發生例外,其他處理仍能繼續執行,請在閉包內使用 try/catch。下一步
佇列與工作
學習如何將處理透過佇列與工作,非同步地在背景執行