Concurrence et multithreading en JavaScript : comment faire fonctionner le code plus rapidement
JavaScript a historiquement été un langage monothread, mais cela ne signifie pas qu'il ne peut pas traiter plusieurs tâches simultanément. Dans Node.js moderne et les navigateurs, il existe des mécanismes puissants pour travailler avec la concurrence et le multithreading. Comprendre ces concepts est crucial pour créer des applications serveur et client hautes performances.
Dans cet article, nous verrons en quoi la concurrence diffère du parallélisme, comment fonctionne l'Event Loop, quand utiliser Worker Threads et Web Workers, et nous examinerons des exemples pratiques sur Node.js.
1. Event Loop et entrées-sorties non bloquantes
La base de la concurrence en JavaScript est l'Event Loop (boucle d'événements). C'est un mécanisme qui permet d'exécuter des opérations asynchrones (par exemple, la lecture de fichiers, les requêtes à une base de données) sans bloquer le thread principal.
Imaginez une file d'attente : l'Event Loop prend une tâche de la file (call stack), l'exécute, et si elle rencontre une opération asynchrone (par exemple, setTimeout ou fetch), la transmet à l'API Web (dans le navigateur) ou à libuv (dans Node.js). Une fois l'opération terminée, son callback est placé dans la file d'attente des tâches (task queue). L'Event Loop vérifie constamment si la pile est vide, et si c'est le cas, elle prend la tâche suivante de la file.
Exemple de code asynchrone :
const fs = require('fs');
console.log('Début');
fs.readFile('example.txt', 'utf8', (err, data) => { if (err) throw err; console.log('Fichier lu :', data);});
console.log('Fin');
// Sortie :// Début// Fin// Fichier lu : ...Remarquez que console.log('Fin') s'exécute avant la lecture du fichier. C'est ce qu'on appelle les entrées-sorties non bloquantes — le thread principal n'attend pas la fin de l'opération, mais continue son exécution.
2. Web Workers et Worker Threads : le vrai multithreading
Bien que l'Event Loop gère très bien les opérations d'E/S, elle n'aide pas pour les calculs "lourds" (traitement d'images, cryptographie, tris complexes). Si vous exécutez un tel code dans le thread principal, toute l'interface (ou le serveur) se "figera" jusqu'à la fin des calculs.
Pour résoudre ce problème, il existe les Web Workers (dans le navigateur) et les Worker Threads (dans Node.js). Ils permettent d'exécuter du code JavaScript dans des threads séparés du système d'exploitation.
Exemple avec Worker Threads dans Node.js
Créons un fichier worker.js :
const { parentPort } = require('worker_threads');
// Tâche lourde : calcul de la somme des nombreslet sum = 0;for (let i = 0; i < 1e9; i++) { sum += i;}
// Envoi du résultat au thread principalparentPort.postMessage(sum);Fichier principal main.js :
const { Worker } = require('worker_threads');
console.log('Thread principal : lancement du worker');
const worker = new Worker('./worker.js');
worker.on('message', (result) => { console.log('Résultat du worker :', result);});
worker.on('error', (err) => { console.error('Erreur du worker :', err);});
worker.on('exit', (code) => { if (code !== 0) { console.error(`Le worker s'est terminé avec le code ${code}`); }});
console.log('Thread principal : nous continuons le travail...');
// Sortie :// Thread principal : lancement du worker// Thread principal : nous continuons le travail...// Résultat du worker : 499999999500000000Comme vous pouvez le voir, le thread principal n'est pas bloqué, et le worker effectue les calculs en arrière-plan. C'est ce qu'on appelle le vrai multithreading.
3. Async/Await et Promise : gestion de la concurrence
Le JavaScript moderne offre des moyens élégants de travailler avec le code asynchrone — async/await et Promise. Ils ne créent pas de nouveaux threads, mais