Skip to content

Panoramica di Blocking vs Non-Blocking

Questa panoramica tratta la differenza tra chiamate blocking e non-blocking in Node.js. Questa panoramica farà riferimento all'event loop e a libuv, ma non sono necessarie conoscenze pregresse su questi argomenti. Si presume che i lettori abbiano una conoscenza di base del linguaggio JavaScript e del pattern di callback di Node.js [/guide/javascript-asynchronous-programming-and-callbacks].

INFO

"I/O" si riferisce principalmente all'interazione con il disco e la rete del sistema supportati da libuv.

Blocking

Blocking si verifica quando l'esecuzione di ulteriore JavaScript nel processo Node.js deve attendere il completamento di un'operazione non JavaScript. Ciò accade perché l'event loop non è in grado di continuare a eseguire JavaScript mentre si sta verificando un'operazione blocking.

In Node.js, JavaScript che mostra prestazioni scadenti a causa di un utilizzo intensivo della CPU piuttosto che dell'attesa di un'operazione non JavaScript, come l'I/O, non è tipicamente definito come blocking. I metodi sincroni nella libreria standard di Node.js che utilizzano libuv sono le operazioni blocking più comunemente usate. Anche i moduli nativi possono avere metodi blocking.

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

Confronto del codice

I metodi Blocking vengono eseguiti sincrònamente e i metodi non-blocking vengono eseguiti asincronamente.

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

js
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // blocca qui fino a quando il file non viene letto

Ed ecco un esempio asincrono equivalente:

js
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 che la seconda riga blocca l'esecuzione di qualsiasi ulteriore JavaScript fino a quando l'intero file non viene letto. Si noti che nella versione sincrona, se viene generata un'eccezione, dovrà essere intercettata o il processo si arresterà. Nella versione asincrona, spetta all'autore decidere se un errore deve essere generato come mostrato.

Espandiamo un po' il nostro esempio:

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

Ed ecco un esempio asincrono simile, ma non equivalente:

js
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 verrà chiamato prima di moreWork(). Nel secondo esempio fs.readFile() è non-blocking, quindi l'esecuzione di JavaScript può continuare e moreWork() verrà chiamato per primo. La possibilità di eseguire moreWork() senza attendere il completamento della lettura del file è una scelta di progettazione chiave che consente una maggiore produttività.

Concorrenza e Throughput

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

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

Il loop degli eventi è diverso dai modelli in molti altri linguaggi in cui è possibile creare thread aggiuntivi per gestire il lavoro concorrente.

Pericoli della miscelazione di codice bloccante e non bloccante

Esistono alcuni pattern che dovrebbero essere evitati quando si gestisce l'I/O. Diamo un'occhiata a un esempio:

js
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, fs.unlinkSync() è probabile che venga eseguito prima di fs.readFile(), il che eliminerebbe file.md prima che venga effettivamente letto. Un modo migliore per scrivere questo, che è completamente non bloccante e garantisce l'esecuzione nell'ordine corretto, è:

js
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 della callback di fs.readFile(), il che garantisce l'ordine corretto delle operazioni.