Tech

Thread in Node.JS

11/10/2025

15 min read

Node.JS è un ambiente di runtime popolare per l'esecuzione di codice JavaScript al di fuori del browser, noto principalmente per la sua architettura non bloccante e basata sugli eventi.

Sebbene Node.JS sia per sua natura single-threaded, offre diversi meccanismi per sfruttare le funzionalità multithreading per l'ottimizzazione delle prestazioni.

NodeJS stesso è multithread e fornisce thread nascosti, usando la libreria libuv.

Gestisce operazioni di I/O come la lettura di file da un disco o richieste di rete. Attraverso questi thread nascosti, NodeJS fornisce metodi asincroni che consentono al codice di effettuare richieste di I/O senza bloccare il thread principale.

Nonostante Node.JS abbia alcuni thread nascosti, non è possibile utilizzarli per alleggerire il carico di lavoro di attività che richiedono molta CPU, come calcoli complessi o il ridimensionamento delle immagini. L'unico modo per velocizzare un'attività che richiede molta CPU è aumentare la "velocità del processore".

Negli ultimi anni, i computer sono stati dotati di core aggiuntivi (dual-core, quad-core, octa-core, ecc.). Per sfruttare questi core,

Node.JS ha introdotto il modulo worker-threads , che consente di creare thread ed eseguire più attività JS in parallelo.

Una volta che un thread completa un'attività, invia un messaggio al thread principale contenente il risultato dell'operazione, in modo che possa essere utilizzato con altre parti del codice.

Il vantaggio dell'utilizzo dei worker-threads è che le attività legate alla CPU non bloccano il thread principale e puoi suddividere e distribuire un'attività tra più worker per ottimizzarla.


Per una comprensione più approfondita, vediamo innanzitutto la differenza tra processo e thread.

Processo

È un programma in esecuzione nel sistema operativo.

Dispone di una propria memoria e non può vedere né accedere alla memoria di altri programmi in esecuzione. Dispone inoltre di un puntatore all'istruzione, che indica l'istruzione attualmente in esecuzione in un programma. Può essere eseguita una sola attività alla volta.

Esempio: utilizzo child_process per creare un nuovo processo

javascript
1const { fork } = require ( 'child_process' ); 2// Crea un nuovo processo Node.js 3const child = fork ( 'child.js' ); 4// Invia un messaggio al processo 5 figlio child. send ({ hello : 'world' }); 6// Ricevi messaggi dal processo 7 figlio child. on ( 'message' , ( message ) => { 8 console . log ( 'Messaggio ricevuto da child:' , message); 9});

child.js

javascript
1process.on('message', (message) => { 2 console.log('Received message from parent:', message); 3 process.send({ reply: 'hello parent' }); 4});

In questo esempio:

  • Lo script padre crea un nuovo processo utilizzando fork('child.js').
  • I processi padre e figlio comunicano tramite messaggi.
  • Ogni processo ha il proprio spazio di memoria.

Thread

I thread sono come i processi: hanno il proprio puntatore alle istruzioni ed eseguono un'attività JS alla volta, ma la differenza fondamentale consiste nel fatto che i thread non hanno una propria memoria. Risiedono nella memoria del processo, dove un processo può contiene più thread.

I thread possono comunicare tra loro tramite scambio di messaggi o condivisione dei dati nella memoria del processo.

Per quanto riguarda l'esecuzione dei thread, il loro comportamento è simile a quello dei processi: se si hanno più thread in esecuzione su un sistema a singolo core, il sistema operativo li passerà da uno all'altro a intervalli regolari, dando a ciascun thread la possibilità di essere eseguito direttamente sulla singola CPU.

Su un sistema multi-core, il sistema operativo pianifica i thread su tutti i core ed esegue il codice JavaScript contemporaneamente. Se si creano più thread rispetto ai core disponibili, ogni core eseguirà più thread contemporaneamente.

Esempio: utilizzo worker_threads percreare un nuovo thread

javascript
1const { Worker , isMainThread, parentPort, workerData } = require ( 'worker_threads' ); 2if (isMainThread) { 3 // Questo è il thread principale 4 const worker = new Worker (__filename, { 5 workerData : { start : 1 , end : 1e6 } 6 }); 7 // Ascolta i messaggi dal worker worker 8 . on ( 'message' , ( result ) => { 9 console . log ( `Risultato dal worker: ${result} ` ); 10 }); 11 worker. on ( 'error' , ( error ) => { 12 console . error ( 'Errore del worker:' , error); 13 }); 14 worker. on ( 'exit' , ( code ) => { 15 if (code !== 0 ) console . error ( `Worker arrestato con codice di uscita ${code} ` ); 16 }); 17} else { 18 // Questo è il thread di lavoro 19 const { inizio, fine } = workerData; 20 let sum = 0 ; 21 for ( let i = inizio; i <= fine; i++) { 22 sum += i; 23 } 24 // Invia il risultato al thread principale 25 parentPort. postMessage (sum); 26} 27

In questo esempio:

  • Il thread principale crea un thread di lavoro utilizzando new Worker(...).
  • Il thread worker esegue un calcolo e invia il risultato al thread principale.
  • Entrambi i thread condividono lo stesso spazio di memoria, consentendo una condivisione efficiente dei dati.

Ora che abbiamo compreso come funzionano i threads, andiamo a fondo con l'argomento principale.

Comprendere i thread nascosti in NodeJS

Node.JS fornisce thread aggiuntivi, motivo per cui è considerato multithread.

Node.JS implementa la libreria libuv , che fornisce quattro thread aggiuntivi a un processo node. Con questi thread, le operazioni di I/O vengono gestite separatamente e, al termine, il ciclo di eventi aggiunge il callback associato all'attività di I/O nella coda dei microtask.

Quando lo stack delle chiamate nel thread principale è libero, il callback associato all'attività di I/O specificata non viene eseguito in parallelo; tuttavia, l'attività stessa di lettura di un file o di una richiesta di rete avviene con l'ausilio dei thread. Una volta completata l'attività di I/O, il callback viene eseguito nel thread principale. Ora guarda l'immagine precedente per una maggiore comprensione.

Oltre a questi quattro thread, il motore V8 fornisce anche due thread per la gestione di attività come la garbage collection automatica . Questo porta il numero totale di thread in un processo a sette : un thread principale, quattro thread Node.js e due thread V8.

Scaricamento di un'attività vincolata alla CPU con il modulo worker-threads

In questa sezione, sposteremo un'attività che richiede un uso intensivo della CPU su un altro thread utilizzando il modulo worker-threads per evitare di bloccare il thread principale.

Per fare ciò, creeremo un worker.jsfile che conterrà l'attività che richiede un uso intensivo della CPU. Nel file index.js, utilizziamo il modulo worker-threads per inizializzare il thread e avviare l'attività nel file worker.js in modo che venga eseguita in parallelo al thread principale. Una volta completata l'attività, il thread di lavoro invierà un messaggio contenente il risultato al thread principale.

Se vengono visualizzati due o più core, è possibile procedere con questo passaggio.

Successivamente, crea e apri il file worker.js nel tuo editor di testo:

1 2cd worker.js 3

Nel tuo file worker.js, aggiungi il seguente codice per importare il modulo worker-threads ed eseguire l'attività che richiede un uso intensivo della CPU:

multi-threading_demo/worker.js

javascript
1const { parentPort } = require ( "worker_threads" ); 2 3let counter = 0 ; 4for ( let i = 0 ; i < 20_000_000_000 ; i++) { 5 counter++; 6}

La prima riga carica il modulo worker_threads ed estrae la classe parentPort. La classe fornisce metodi che è possibile utilizzare per inviare messaggi al thread principale. Successivamente, si trova l'attività ad alta intensità di CPU attualmente contenuta nella calculateCount()funzione nel index.jsfile. Più avanti in questo passaggio, si eliminerà questa funzione da index.js.

Successivamente, aggiungi il codice evidenziato qui sotto:

multi-threading_demo/worker.js

javascript
1const { parentPort } = require ( "worker_threads" ); 2 3let counter = 0 ; 4for ( let i = 0 ; i < 20_000_000_000 ; i++) { 5 counter++; 6} 7parentPort. postMessage (counter);

Qui si richiama il metodo postMessage() della classe parentPort, che invia un messaggio al thread principale contenente il risultato dell'attività associata alla CPU memorizzata nella countervariabile.

Salva ed esci dal file. Aprilo index.jsnel tuo editor di testo:

1cd index.js

Poiché hai già l'attività vincolata alla CPU in worker.js, rimuovi il codice evidenziato da index.js:

multi-threading_demo/index.js

javascript
1const express = require ( "express" ); 2 3const app = express (); 4const port = process. env . PORT || 3000 ; 5app. get ( "/non bloccante/" , ( req, res ) => { 6 res. status ( 200 ). send ( "Questa pagina non bloccante" ); 7}); 8function calculateCount ( ) { 9 return new Promise ( ( resolve, reject ) => { 10 let counter = 0 ; 11 for ( let i = 0 ; i < 20_000_000_000 ; i++) { 12 counter++; 13 } 14 resolve (counter); 15 }); 16} 17app. get ( "/bloccante" , async (req, res) => { 18 const counter = await calculateCount (); 19 res. status ( 200 ). send ( `result is ${counter} ` ); 20}); 21app. ascolta (porta, () => { 22 console . log ( `App in ascolto sulla porta ${porta} ` ); 23});

Successivamente, nella callback app.get("/blocking"), aggiungi il seguente codice per inizializzare il thread:

multi-threading_demo/index.js

javascript
1const express = require ( "express" ); 2const { Worker } = require ( "worker_threads" ); 3... 4app. get ( "/blocking" , async (req, res) => { 5 const worker = new Worker ( "./worker.js" ); 6 worker. on ( "message" , ( data ) => { 7 res. status ( 200 ). send ( `result is ${data} ` ); 8 }); 9 worker. on ( "error" , ( msg ) => { 10 res. status ( 404 ). send ( `Si è verificato un errore: ${msg} ` ); 11 }); 12}); 13...

Per prima cosa, si importa il modulo worker_threads e si scompatta la classe Worker.

All'interno della callback app.get("/blocking"), si crea un'istanza di Worker utilizzando la parola chiave new, seguita da una chiamata a Worker con il worker.js percorso del file come argomento. Questo crea un nuovo thread e il codice nel worker.jsfile inizia a essere eseguito nel thread su un altro core.

Successivamente, si associa un evento all'istanza worker utilizzando il metodo on("message") per ascoltare l'evento messaggio.

Quando il messaggio contenente il risultato del file worker.js viene ricevuto, viene passato come parametro alla callback del metodo, che restituisce una risposta all'utente contenente il risultato dell'attività vincolata alla CPU.

Successivamente, si associa un altro evento all'istanza worker utilizzando il metodo on("error") per ascoltare l'evento di errore. Se si verifica un errore, il callback restituisce 404 all'utente una risposta contenente il messaggio di errore.

Il file completo apparirà ora come segue:

multi-threading_demo/index.js

javascript
1const express = require ( "express" ); 2const { Worker } = require ( "worker_threads" ); 3const app = express (); 4const port = process. env . PORT || 3000 ; 5app. get ( "/non bloccante/" , ( req, res ) => { 6 res. status ( 200 ). send ( "Questa pagina non bloccante" ); 7}); 8app. get ( "/blocking" , async (req, res) => { 9 const worker = new Worker ( "./worker.js" ); 10 worker. on ( "messaggio" , ( dati ) => { 11 res. status ( 200 ). send ( `il risultato è ${data} ` ); 12 }); 13 worker. on ( "errore" , ( msg ) => { 14 res. status ( 404 ). send ( `Si è verificato un errore: ${msg} ` ); 15 }); 16}); 17app. listen (porta, () => { 18 console . log ( `App in ascolto sulla porta ${porta} ` ); 19});

Salva ed esci dal file, quindi esegui il server:

1node index.js

Blocking endpoint:

Osserva che le richieste di blocco hanno impiegato solo 6,93 secondi per caricarsi senza bloccare l'endpoint non bloccante, che ha impiegato solo 7 ms quando entrambe le richieste sono state effettuate contemporaneamente.

Non blocking endpoint:

Visitiamo nuovamente la scheda http://localhost:3000/blocking nel browser web. Prima che termini il caricamento, aggiorniamo tutte le schede http://localhost:3000/non-blocking.

Noteremo che ora vengono caricate istantaneamente, senza attendere il /blocking completamento del caricamento del percorso.

Perche?

Perché l'attività legata alla CPU viene trasferita a un altro thread e il thread principale gestisce tutte le richieste in arrivo.

Ora, arrestiamo il server (CTRL+C).

Ora che è possibile rendere non bloccante un'attività che richiede un uso intensivo della CPU utilizzando un thread di lavoro, è possibile utilizzare quattro thread di lavoro per migliorare le prestazioni dell'attività che richiede un uso intensivo della CPU.

Conclusione

Sebbene Node.JS sia intrinsecamente single-threaded, offre meccanismi come i thread nascosti tramite la libreria libuv per operazioni di I/O efficienti e il modulo worker-threads per le attività che richiedono un utilizzo intensivo della CPU. Delegando le attività intensive ai worker thread, è possibile mantenere migliorare significativamente le prestazioni e la scalabilità delle applicazioni.


Risorse

DOC: https://nodejs.org/api/worker_threads.html