Event loop e non-blocking
Prima di iniziare: le parole della lezione
- Call stack ed event loop
- Il call stack tiene traccia delle funzioni in esecuzione. L’event loop coordina quando eseguire callback di timer, rete e altre operazioni, quando lo stack permette di farlo.
- Concorrenza e parallelismo
- Concorrenza significa gestire più attività nello stesso periodo; parallelismo significa eseguirle davvero nello stesso istante su risorse distinte.
- Thread pool e libuv
- Un thread pool è un gruppo di thread riutilizzati per certi lavori. libuv è la libreria su cui Node basa parte della gestione di I/O, event loop e thread pool.
- Microtask e CPU-bound
- Le microtask includono reazioni alle Promise e vengono elaborate in specifici punti dell’esecuzione. CPU-bound indica lavoro dominato dal calcolo, anziché dall’attesa di dati.
L'event loop orchestra l'esecuzione del codice e dei callback quando le operazioni asincrone terminano. Non confondere asincronia, parallelismo e multithreading: il codice JavaScript di un processo Node normalmente gira su un thread principale, mentre parte del lavoro I/O è delegata al sistema operativo o al thread pool.
console.log('A');
setTimeout(() => console.log('C (timer)'), 0);
Promise.resolve().then(() => console.log('B (microtask)'));
console.log('D');
// A, D, B, C
Esempio aggiuntivo: await readFile(...) lascia spazio ad altre operazioni durante l'attesa; un ciclo while che occupa la CPU per molti secondi blocca comunque l'event loop. Per calcoli pesanti valuta worker thread o processi separati.
Chi fa cosa, e perché
Nella banca, lo sportellista non si moltiplica: coordina le pratiche. Allo stesso modo il JavaScript principale esegue un pezzo di codice alla volta; il sistema operativo può gestire attese di rete e il thread pool alcune operazioni, fra cui molte operazioni sui file. Dire “ogni richiesta ha un thread” oppure “Node usa un solo thread in assoluto” descrive male il funzionamento.
Nell’esempio A/D/B/C, le due stampe sincrone vengono prima della reazione della Promise; il timer non può interrompere il codice già in esecuzione. setTimeout(fn, 0) significa pianificare appena possibile dopo la soglia del timer, non chiamare subito fn. La parola async non sposta un ciclo pesante su un altro thread.
Un altro esempio guidato
// attese.mjs — node attese.mjs
import { setTimeout as pausa } from 'node:timers/promises';
async function lavoro(nome, ms) {
await pausa(ms);
return nome;
}
const risultati = await Promise.all([
lavoro('lento', 80),
lavoro('veloce', 20)
]);
console.log(risultati);
Come leggerlo e risultato atteso. Le due attese vengono avviate prima di attenderne l’esito insieme. Il risultato è ["lento", "veloce"]: Promise.all preserva l’ordine degli input, non quello di completamento. Se una Promise fallisce, l’attesa complessiva fallisce; le altre operazioni non sono cancellate automaticamente.
Prova tu. Sostituisci Promise.all con due await consecutivi e confronta il tempo complessivo: nel primo caso è vicino all’attesa maggiore, nel secondo alla somma, con variazioni dovute alla schedulazione.