Concurrency::run() in Laravel: esecuzione parallela di task I/O-bound senza code, worker o estensioni pcntl
In una piattaforma marketplace con migliaia di utenti attivi, un comando Artisan di sincronizzazione notturna chiamava 5 API esterne in sequenza per ogni ordine: verifica pagamento, aggiornamento inventario, calcolo spedizione, generazione fattura, notifica al fornitore. Con 200 ordini da processare e una latenza media di 800ms per chiamata API, il comando impiegava 13 minuti - 200 ordini × 5 chiamate × 0.8s. Le 5 chiamate per ogni ordine erano indipendenti tra loro, ma il codice le eseguiva una dopo l'altra perché PHP non offre concorrenza nativa senza estensioni. La legge di Amdahl formalizza il problema: se il 95% del tempo è speso in attesa di I/O parallelizzabile, il massimo speedup teorico con 5 task paralleli è 3.6× - da 13 minuti a meno di 4.
Come funziona Concurrency::run() internamente?
Concurrency::run(), introdotto in Laravel 11.23 e stabilizzato in Laravel 12, non usa fibers né thread. Il driver process (default) serializza ogni closure, la invia a un comando Artisan nascosto che la esegue in un processo PHP figlio separato, e restituisce il risultato serializzato al processo padre. Ogni processo figlio esegue il bootstrap completo dell'applicazione - service container, config, database connection - il che significa un overhead di 10-70ms per task.
Il driver fork usa spatie/fork per creare processi tramite pcntl_fork - più veloce perché evita il bootstrap ripetuto, ma richiede l'estensione pcntl (non disponibile su Windows e in molti ambienti FPM) ed è utilizzabile solo in CLI. Il driver sync esegue tutto sequenzialmente - utile per i test.
L'API è dichiarativa: un array di closure in input, un array di risultati in output nello stesso ordine:
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\Http;
/* Sincronizzazione ordine: 5 API chiamate in parallelo invece che in sequenza */
$results = Concurrency::run([
fn () => Http::shipping()->get("/rates/{$order->id}")->json(),
fn () => Http::payments()->get("/verify/{$order->payment_id}")->json(),
fn () => Http::inventory()->post('/reserve', ['items' => $order->items])->json(),
fn () => Http::invoicing()->post('/generate', ['order' => $order->id])->json(),
fn () => Http::suppliers()->post('/notify', ['order' => $order->id])->json(),
]);
[$rates, $payment, $inventory, $invoice, $supplier] = $results;Le macro HTTP (Http::shipping(), Http::payments()) centralizzano la configurazione di ogni servizio esterno. Concurrency::run() le esegue in parallelo - il tempo totale è quello della chiamata più lenta, non la somma di tutte e cinque.
Concurrency::defer() ha un caso d'uso diverso: esegue closure dopo che la risposta HTTP è stata inviata al client, in modalità fire-and-forget senza valore di ritorno. È utile per operazioni post-response come analytics, cache warming o cleanup.
Quali sono i vincoli di Concurrency::run() che è essenziale conoscere?
Il primo vincolo è la serializzazione: le closure passate a Concurrency::run() devono essere serializzabili. Referenze a $this, connessioni database aperte, risorse file handle o oggetti non serializzabili causano un errore a runtime. Il pattern corretto è passare solo dati scalari o model ID nella closure, e risolvere le dipendenze all'interno:
/* SBAGLIATO: $this non è serializzabile */
Concurrency::run([
fn () => $this->service->process($order),
]);
/* CORRETTO: risolvere nel processo figlio */
$orderId = $order->id;
Concurrency::run([
function () use ($orderId) {
$service = app(OrderService::class);
$order = Order::find($orderId);
return $service->process($order);
},
]);Il secondo è l'isolamento dei processi: ogni task gira in un processo separato con la propria connessione database. Le transazioni del processo padre non sono visibili ai figli, e viceversa. Se un task scrive nel database, gli altri task non vedranno quella scrittura fino al commit. Per operazioni che richiedono consistenza transazionale, le code Laravel con job sequenziali sono l'alternativa corretta.
Il terzo è il costo del bootstrap: con il driver process, ogni task spawna un processo PHP che carica l'intera applicazione. Per 5 chiamate API da 800ms ciascuna, l'overhead di 50ms è trascurabile. Per 100 task da 10ms ciascuno, l'overhead (100 × 50ms = 5s) supera il tempo dei task stessi. La regola pratica: Concurrency::run() conviene quando il tempo di I/O per task è almeno 10× l'overhead di bootstrap.
Il quarto è la gestione dei fallimenti: se un task lancia un'eccezione, Concurrency::run() la propaga al processo padre. I risultati dei task completati con successo vanno persi. Per task dove i fallimenti parziali sono accettabili, ogni closure deve gestire le proprie eccezioni internamente e restituire un risultato che indichi successo o fallimento.
Errori comuni nell'uso della concorrenza
Il primo errore è confondere concorrenza con code. Concurrency::run() è sincrono dal punto di vista del processo chiamante - blocca finché tutti i task non completano. Le code Laravel sono asincrone - il dispatch ritorna immediatamente e il job viene eseguito da un worker separato. Per operazioni I/O-bound dove serve il risultato immediatamente (aggregare dati da più API), usa Concurrency. Per operazioni fire-and-forget (inviare email, generare PDF), usa le code.
Il secondo è non limitare il numero di task concorrenti. 200 task in parallelo significano 200 processi PHP e 200 connessioni database. Un server con 4 CPU e un pool di 50 connessioni MySQL andrà in sofferenza. Il chunking è la strategia corretta: processare batch di 10-20 task alla volta con collect($tasks)->chunk(15)->each(fn ($batch) => Concurrency::run($batch->all())).
Il terzo è usare Concurrency per task CPU-bound in ambiente web. Il driver process spawna processi che competono per la CPU con il web server - sotto carico, questo può degradare i tempi di risposta per tutti gli utenti. I task CPU-bound (elaborazione immagini, calcoli pesanti) appartengono alle code con worker dedicati, non a Concurrency::run() in una request HTTP.
La concorrenza è uno strumento potente ma con un dominio di applicazione preciso: task I/O-bound indipendenti dove serve il risultato nel processo corrente. Fuori da questo dominio - task CPU-bound, operazioni con requisiti transazionali, fire-and-forget - le code e i job rimangono la scelta corretta. L'ottimizzazione delle query Eloquent e il logging strategico delle metriche di esecuzione completano il quadro per applicativi ad alte prestazioni. Per conoscere il mio approccio all'ottimizzazione di applicativi Laravel, visita la mia pagina professionale. Se il tuo applicativo esegue operazioni batch sequenziali che potrebbero beneficiare della parallelizzazione, contattami per una consulenza dedicata - partiamo dal profiling dei tempi di esecuzione e dall'identificazione dei bottleneck.