📢 Nuovo Corso Laravel API disponibile!

Panoramica di Blocking vs Non-Blocking

Questa panoramica copre la differenza tra chiamate bloccanti e non bloccanti in Node.js. Questa panoramica farà riferimento al ciclo degli eventi e a libuv, ma non è richiesta alcuna conoscenza preventiva di questi argomenti. Si presume che i lettori abbiano una comprensione di base del linguaggio JavaScript e del pattern di callback di Node.js.

”I/O” si riferisce principalmente all’interazione con il disco di sistema e la rete supportata da libuv.

Bloccante

Bloccante è quando l’esecuzione di ulteriore JavaScript nel processo Node.js deve attendere che un’operazione non JavaScript sia completata. Questo accade perché il ciclo degli eventi non può continuare a eseguire JavaScript mentre si verifica un’operazione bloccante.

In Node.js, il JavaScript che mostra una scarsa performance a causa di una intensità CPU piuttosto che di un’attesa su un’operazione non JavaScript, come l’I/O, non è tipicamente definito come bloccante. I metodi sincroni nella libreria standard di Node.js che utilizzano libuv sono le operazioni bloccanti più comunemente utilizzate. I moduli nativi possono anche avere metodi bloccanti.

Tutti i metodi I/O nella libreria standard di Node.js forniscono versioni asincrone, che sono non bloccanti, e accettano funzioni di callback. Alcuni metodi hanno anche controparti bloccanti, che hanno nomi che terminano con Sync.

Confronto del Codice

I metodi bloccanti eseguono sincronamente e i metodi non bloccanti eseguono asincronamente.

Utilizzando il modulo File System come esempio, questa è una lettura di file sincrona:

const fs = require("node:fs");
const data = fs.readFileSync("/file.md"); // si blocca qui fino a quando il file viene letto

Ed ecco un esempio equivalente asincrono:

const fs = require("node:fs");
fs.readFile("/file.md", (err, data) => {
if (err) throw err;
});

Il primo esempio appare più semplice del secondo, ma ha lo svantaggio della seconda riga che blocca l’esecuzione di qualsiasi altro JavaScript fino a quando l’intero file non viene letto. Nota che nella versione sincrona se si verifica un errore, sarà necessario catturarlo o il processo si bloccherà. Nella versione asincrona, spetta all’autore decidere se un errore dovrebbe essere gestito come mostrato.

Espandiamo un po’ il nostro esempio:

const fs = require("node:fs");
const data = fs.readFileSync("/file.md"); // si blocca qui fino a quando il file viene letto
console.log(data);
moreWork(); // verrà eseguito dopo console.log

Ed ecco un esempio asincrono simile, ma non equivalente:

const fs = require("node:fs");
fs.readFile("/file.md", (err, data) => {
if (err) throw err;
console.log(data);
});
moreWork(); // verrà eseguito prima di console.log

Nel primo esempio sopra, console.log sarà chiamato prima di moreWork(). Nel secondo esempio, fs.readFile() è non bloccante, quindi l’esecuzione di JavaScript può continuare e moreWork() verrà chiamato prima. La possibilità di eseguire moreWork() senza attendere che la lettura del file sia completata è una scelta progettuale fondamentale che consente un maggiore throughput.

Concorrenza e Throughput

L’esecuzione di JavaScript in Node.js è single-threaded, quindi la concorrenza si riferisce alla capacità del ciclo degli eventi di eseguire le funzioni di callback JavaScript dopo aver completato altri lavori. Qualsiasi codice che si prevede debba essere eseguito in modo concorrente deve consentire al ciclo degli eventi di continuare a eseguire JavaScript mentre si verificano operazioni non JavaScript, come l’I/O.

Ad esempio, consideriamo un caso in cui ogni richiesta a un server web impiega 50 ms per essere completata e 45 ms di questi 50 ms sono I/O del database che possono essere eseguiti in modo asincrono. Scegliere operazioni asincrone non bloccanti libera quei 45 ms per richiesta per gestire altre richieste. Questa è una differenza significativa in capacità solo scegliendo di utilizzare metodi non bloccanti invece di metodi bloccanti.

Il ciclo degli eventi è diverso dai modelli in molte altre lingue in cui potrebbero essere creati thread aggiuntivi per gestire il lavoro concorrente.

Pericoli della Miscelazione di Codice Bloccante e Non Bloccante

Ci sono alcuni schemi che dovrebbero essere evitati quando si tratta di I/O. Vediamo un esempio:

const fs = require("node:fs");
fs.readFile("/file.md", (err, data) => {
if (err) throw err;
console.log(data);
});
fs.unlinkSync("/file.md");

Nell’esempio sopra, è probabile che fs.unlinkSync() venga eseguito prima di fs.readFile(), cancellando file.md prima che venga effettivamente letto. Un modo migliore per scrivere questo, completamente non bloccante e garantito per eseguirsi nell’ordine corretto è:

const fs = require("node:fs");
fs.readFile("/file.md", (readFileErr, data) => {
if (readFileErr) throw readFileErr;
console.log(data);
fs.unlink("/file.md", (unlinkErr) => {
if (unlinkErr) throw unlinkErr;
});
});

Il codice sopra inserisce una chiamata non bloccante a fs.unlink() all’interno del callback

di fs.readFile(), garantendo l’ordine corretto delle operazioni.