Skip to content

Vue d'ensemble des appels bloquants et non bloquants

Cet aperçu couvre la différence entre les appels bloquants et non bloquants dans Node.js. Cet aperçu fera référence à la boucle d'événements et à libuv, mais aucune connaissance préalable de ces sujets n'est requise. Les lecteurs sont supposés avoir une compréhension de base du langage JavaScript et du modèle de callback de Node.js.

INFO

"I/O" fait principalement référence à l'interaction avec le disque et le réseau du système, pris en charge par libuv.

Bloquant

Bloquant signifie que l'exécution de JavaScript supplémentaire dans le processus Node.js doit attendre qu'une opération non JavaScript se termine. Cela se produit parce que la boucle d'événements ne peut pas continuer à exécuter JavaScript lorsqu'une opération bloquante est en cours.

Dans Node.js, le JavaScript qui présente de mauvaises performances en raison d'une intensité CPU élevée plutôt que d'attendre une opération non JavaScript, telle que les E/S, n'est généralement pas considéré comme bloquant. Les méthodes synchrones de la bibliothèque standard Node.js qui utilisent libuv sont les opérations bloquantes les plus couramment utilisées. Les modules natifs peuvent également avoir des méthodes bloquantes.

Toutes les méthodes d'E/S de la bibliothèque standard Node.js fournissent des versions asynchrones, qui sont non bloquantes, et acceptent des fonctions de rappel. Certaines méthodes ont également des équivalents bloquants, dont les noms se terminent par Sync.

Comparaison de code

Les méthodes bloquantes s'exécutent synchronément et les méthodes non bloquantes s'exécutent asynchronemement.

En utilisant le module File System comme exemple, voici une lecture de fichier synchrone :

js
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // bloque ici jusqu'à ce que le fichier soit lu

Et voici un exemple asynchrone équivalent :

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

Le premier exemple semble plus simple que le second, mais il a l'inconvénient que la deuxième ligne bloque l'exécution de tout JavaScript supplémentaire jusqu'à ce que le fichier entier soit lu. Notez que dans la version synchrone, si une erreur est levée, elle devra être interceptée ou le processus se bloquera. Dans la version asynchrone, il appartient à l'auteur de décider si une erreur doit être levée comme indiqué.

Développons un peu notre exemple :

js
const fs = require('node:fs')
const data = fs.readFileSync('/file.md') // bloque ici jusqu'à ce que le fichier soit lu
console.log(data)
moreWork() // s'exécutera après console.log

Et voici un exemple asynchrone similaire, mais pas équivalent :

js
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
  if (err) throw err
  console.log(data)
})
moreWork() // s'exécutera avant console.log

Dans le premier exemple ci-dessus, console.log sera appelé avant moreWork(). Dans le deuxième exemple, fs.readFile() est non bloquant, donc l'exécution JavaScript peut continuer et moreWork() sera appelé en premier. La possibilité d'exécuter moreWork() sans attendre la fin de la lecture du fichier est un choix de conception clé qui permet un débit plus élevé.

Concurrence et Débit

L'exécution JavaScript dans Node.js est monothreadée, donc la concurrence se réfère à la capacité de la boucle d'événements à exécuter les fonctions de rappel JavaScript après avoir terminé d'autres tâches. Tout code qui doit s'exécuter de manière concurrente doit permettre à la boucle d'événements de continuer à fonctionner pendant que des opérations non JavaScript, comme les E/S, sont en cours.

Prenons par exemple le cas où chaque requête à un serveur web prend 50 ms à se terminer et que 45 ms de ces 50 ms correspondent à des E/S de base de données pouvant être effectuées de manière asynchrone. Le choix d'opérations asynchrones non bloquantes libère ces 45 ms par requête pour gérer d'autres requêtes. Il s'agit d'une différence de capacité significative simplement en choisissant d'utiliser des méthodes non bloquantes au lieu de méthodes bloquantes.

La boucle d'événements est différente des modèles de nombreux autres langages où des threads supplémentaires peuvent être créés pour gérer les travaux concurrents.

Dangers du Mélange de Code Bloquant et Non Bloquant

Il existe certains modèles à éviter lorsqu'on traite des E/S. Prenons un exemple :

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

Dans l'exemple ci-dessus, fs.unlinkSync() sera probablement exécuté avant fs.readFile(), ce qui supprimerait file.md avant même qu'il ne soit lu. Une meilleure façon d'écrire ceci, qui est complètement non bloquante et garantit l'exécution dans le bon ordre, est :

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
  })
})

Ce qui précède place un appel non bloquant à fs.unlink() dans la fonction de rappel de fs.readFile(), ce qui garantit le bon ordre des opérations.