Introduzione
Quando lavoriamo con Async/Await, il codice diventa molto più leggibile rispetto alle Promise concatenate. Però c’è un problema pratico che compare spesso nei progetti reali: non sempre conviene eseguire tutto in parallelo.
Immagina di dover elaborare centinaia di elementi: inviare richieste API, leggere file, fare controlli su un database o scaricare immagini. Se lanci tutte le operazioni insieme, rischi di saturare la rete, il server o la memoria. Se invece le esegui una alla volta, il programma diventa troppo lento.
Qui entra in gioco un pattern molto utile: la concorrenza limitata (o concurrency limit). L’idea è semplice: eseguire più operazioni asincrone in parallelo, ma con un numero massimo controllato di task attivi contemporaneamente.
Questo approccio è perfetto per scrivere codice asincrono più leggibile e più robusto, perché unisce i vantaggi di Async/Await con un controllo preciso sulle risorse. In questo tutorial vedremo come implementarlo in JavaScript con un esempio concreto e riutilizzabile.
Codice completo
// Esempio: elaboriamo una lista di utenti, ma senza avviare troppi task insieme.
// Obiettivo: massimo 3 operazioni contemporanee.
const utenti = [
{ id: 1, nome: "Anna" },
{ id: 2, nome: "Luca" },
{ id: 3, nome: "Marta" },
{ id: 4, nome: "Giulia" },
{ id: 5, nome: "Paolo" },
{ id: 6, nome: "Sara" }
];
// Simula una chiamata asincrona lenta
async function caricaProfiloUtente(utente) {
const ritardo = Math.floor(Math.random() * 1500) + 500;
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: utente.id,
nome: utente.nome,
stato: "profilo caricato",
tempo: ritardo
});
}, ritardo);
});
}
// Funzione generica per gestire la concorrenza limitata
async function eseguiConLimite(elementi, limite, task) {
const risultati = [];
let indiceCorrente = 0;
async function worker() {
while (indiceCorrente < elementi.length) {
const indice = indiceCorrente++;
const elemento = elementi[indice];
try {
const risultato = await task(elemento);
risultati[indice] = risultato;
} catch (errore) {
risultati[indice] = {
errore: true,
messaggio: errore.message
};
}
}
}
const workers = [];
for (let i = 0; i < limite; i++) {
workers.push(worker());
}
await Promise.all(workers);
return risultati;
}
// Uso della funzione
async function main() {
console.log("Avvio elaborazione...");
const risultati = await eseguiConLimite(utenti, 3, caricaProfiloUtente);
console.log("Risultati finali:");
console.log(risultati);
}
main(); Spiegazione
Vediamo il funzionamento del codice passo per passo.
1. Il problema da risolvere
Abbiamo un array di utenti e una funzione asincrona caricaProfiloUtente che simula un’operazione lenta, come una richiesta HTTP. Se usassimo Promise.all su tutti gli elementi, partirebbero troppe operazioni insieme. Se usassimo un ciclo con await dentro, eseguiremmo tutto in serie, quindi più lentamente.
2. La funzione eseguiConLimite
Questa funzione è il cuore del pattern. Riceve:
- elementi: la lista da elaborare
- limite: il numero massimo di task contemporanei
- task: la funzione asincrona da eseguire per ogni elemento
L’idea è creare più worker, cioè piccoli processi asincroni che pescano elementi dall’array finché ce ne sono disponibili.
3. Il ruolo di indiceCorrente
La variabile indiceCorrente indica il prossimo elemento da processare. Ogni worker prende un indice, lo incrementa e avvia il task corrispondente.
In questo modo non serve dividere manualmente l’array. I worker si distribuiscono il lavoro in modo dinamico: quando uno termina, passa al prossimo elemento disponibile.
4. Perché usiamo Promise.all(workers)
Anche se ogni worker lavora in modo asincrono, dobbiamo comunque aspettare che tutti abbiano finito. Promise.all serve proprio a questo: aspetta la conclusione di tutti i worker avviati.
5. Perché i risultati sono salvati per indice
Quando le operazioni asincrone finiscono in tempi diversi, i risultati potrebbero arrivare in ordine casuale. Salvandoli in risultati[indice], manteniamo l’ordine originale degli elementi. Questo è molto importante in applicazioni reali, ad esempio quando devi mostrare i dati nello stesso ordine dell’input.
6. Gestione degli errori
Nel worker abbiamo un try/catch. Se una singola operazione fallisce, non blocchiamo tutto il processo: registriamo l’errore nel risultato corrispondente e continuiamo con gli altri elementi. È una scelta spesso migliore rispetto a interrompere completamente l’elaborazione.
Best practice
Il pattern di concorrenza limitata è utile, ma va usato con attenzione. Ecco alcune buone pratiche da seguire.
- Scegli il limite in base alla risorsa da proteggere: per una API esterna, un limite basso può evitare rate limit o blocchi temporanei; per operazioni locali, puoi permetterti un valore più alto.
- Non usare Promise.all su grandi volumi senza controllo: è veloce, ma può generare troppa pressione su rete, CPU o memoria.
- Mantieni l’ordine dei risultati se serve: in molti casi l’ordine di input deve essere preservato, soprattutto in UI o report.
- Gestisci gli errori per singolo task: così un errore isolato non interrompe l’intero flusso.
- Rendi la funzione riutilizzabile: separare il motore di concorrenza dalla logica applicativa migliora manutenzione e test.
- Usa nomi chiari: variabili come
worker,limiteeindiceCorrenteaiutano a capire subito il comportamento del codice.
Un altro consiglio importante: se lavori in un contesto dove la concorrenza è critica, misura sempre il comportamento reale. Il valore migliore del limite non è universale: dipende dal tipo di operazione, dal server, dal browser e dalle condizioni di rete.
Riepilogo
Async/Await rende il codice asincrono più leggibile, ma non risolve automaticamente tutti i problemi di esecuzione. Quando hai molte operazioni da gestire, il pattern della concorrenza limitata è una soluzione pratica e pulita.
Con questo approccio ottieni:
- più controllo sulle risorse;
- codice chiaro e facilmente riutilizzabile;
- elaborazione più efficiente rispetto alla serializzazione completa;
- maggiore stabilità rispetto all’avvio di tutto in parallelo.
In sintesi: Async/Await non serve solo a “scrivere meglio le Promise”, ma anche a costruire flussi asincroni più intelligenti. La concorrenza limitata è uno di quei pattern che fanno davvero la differenza nei progetti reali.
Approfondisci con risorse ufficiali
- MDN Web Docs - async function: documentazione ufficiale sulle funzioni asincrone in JavaScript.
- MDN Web Docs - Promise: riferimento completo sul comportamento delle Promise.
- MDN Web Docs - Promise.all: utile per capire il parallelismo e i suoi limiti.
- ECMAScript Language Specification: specifica del linguaggio per chi vuole approfondire il funzionamento formale di async/await.
