Le multithreading dans Node.js avec les threads worker : tutoriel sur worker_threads
James Walker
25 octobre 2022
0 minutes de lectureNode.js fournit un environnement d’exécution JavaScript à thread unique, qui empêche le code d’exécuter plusieurs opérations en parallèle. Si votre application repose généralement sur une exécution synchrone, les opérations de longue durée peuvent la bloquer.
Node.js est toutefois lui-même une application multithread. Cela se voit lorsque vous utilisez l’une des méthodes asynchrones de la bibliothèque standard pour effectuer des opérations d’E/S, comme la lecture d’un fichier ou l’envoi d’une requête réseau. Ces tâches sont confiées à un pool de threads distinct que Node crée et gère à l’aide de la bibliothèque C libuv. Même si cela ressemble à du multithreading, les fonctions asynchrones peuvent toujours bloquer la boucle d’événements principale.
Mais que faire si vous souhaitez créer vos propres threads indépendants pour votre application Node.js ? Le multithreading peut améliorer considérablement les performances des tâches qui sollicitent le processeur en permettant d’exécuter des opérations arbitraires en parallèle. Bien que Node.js ne propose pas de véritable multithreading, le module worker_threads permet d’en créer une forme similaire. Cet article explique son fonctionnement et montre comment l’utiliser dans quelques applications concrètes.
Que sont les threads worker ?
Le module worker_threads met en œuvre une forme de multithreading qui vous permet d’ajouter du parallélisme à votre application. Le code exécuté dans un thread worker s’exécute dans un processus enfant distinct, ce qui l’empêche de bloquer votre application principale.
Les threads worker ne sont pas de véritables threads au sens traditionnel du terme. Ce sont des processus distincts, ce qui signifie qu’ils ne peuvent pas accéder directement au contexte d’exécution de leur processus parent. La communication entre les threads worker et votre application repose sur un système de messagerie événementiel.
Même si les threads worker ne transforment pas Node.js en langage véritablement multithread, cette distinction reste théorique dans de nombreux cas concrets. Ils offrent un mécanisme pratique pour exécuter plusieurs « threads » simultanément et déléguer les tâches lourdes en dehors de la boucle principale.
Cas d’usage des threads worker
Les threads worker peuvent être utilisés pour toute opération coûteuse qui sollicite le processeur. Ils ne conviennent pas à l’accélération des opérations d’E/S, en raison de la surcharge associée à chaque thread. Les utilitaires d’E/S asynchrones intégrés à Node.js seront plus rapides et plus efficaces pour les tâches liées au système de fichiers et au réseau.
Les threads worker peuvent être utiles dans de nombreuses situations. Voici quelques cas d’usage particulièrement courants où ils peuvent vous aider :
Redimensionnement d’images : le redimensionnement de grandes images peut prendre plusieurs secondes, et les délais s’accumulent rapidement si vous devez générer plusieurs tailles. C’est courant dans les applications qui convertissent les photos importées en miniatures, ainsi qu’en formats petit et grand. Vous pouvez utiliser trois threads worker pour générer toutes les tailles en même temps et réduire ainsi la durée totale du traitement.
Compression vidéo : la compression vidéo est l’une des tâches de calcul les plus exigeantes. Les threads worker peuvent l’accélérer en traitant plusieurs images en parallèle, puis en renvoyant les résultats au thread principal.
Chiffrement de fichiers et autres opérations cryptographiques : les opérations cryptographiques sont volontairement complexes. Le chiffrement et le déchiffrement de fichiers, la génération de clés secrètes et la vérification de signatures peuvent toutes entraîner des ralentissements perceptibles dans un programme lorsqu’elles sont exécutées sur le thread principal.
Tri et recherche dans de grandes quantités de données : le filtrage et le tri des données nécessitent de nombreuses itérations pour comparer chaque valeur. Les threads worker peuvent accélérer ces opérations en examinant plusieurs ensembles de données en parallèle.
Opérations mathématiques complexes : les calculs mathématiques — comme la génération de nombres premiers, la factorisation de grands nombres et l’analyse complexe de données — sollicitent fortement le processeur. Effectuer une partie du travail dans un thread distinct permet à la boucle principale de traiter d’autres tâches.
Dans tous ces cas, le ralentissement s’explique par le temps que le processeur consacre à l’exécution du code, plutôt qu’à la lecture de données sur le disque ou le réseau. Il s’agit de tâches itératives : augmenter le nombre de passages effectués en parallèle est donc le meilleur moyen d’améliorer les performances. Les threads worker permettent d’y parvenir.
Utiliser le module worker threads
Pour créer des threads worker, importez le module worker_threads de Node et appelez le constructeur new Worker(). Un nouveau processus est lancé pour exécuter le fichier JavaScript que vous indiquez. Vous pouvez échanger des messages avec le worker afin d’être averti lorsqu’un événement se produit, que des données sont prêtes à être traitées ou qu’une erreur survient. Si vous avez déjà utilisé les Web Workers dans un navigateur, le concept de thread worker vous sera familier.
Voici l’exemple le plus simple d’exécution d’un fichier JavaScript dans un thread worker :
Copiez cet extrait de code et enregistrez-le dans un fichier nommé worker-demo.js dans votre répertoire de travail. Lorsque vous exécutez le code avec Node.js, le résultat suivant devrait s’afficher :
Cet exemple simple présente les principes fondamentaux des threads worker. Le code source peut servir aussi bien au thread principal qu’à un thread worker. Le module worker_threads fournit un export isMainThread qui vous permet de vérifier si le code s’exécute sur le thread principal.
Lorsque le fichier s’exécute sur le thread principal — autrement dit, lorsque vous le lancez depuis votre terminal —, la première branche de l’instruction if s’exécute. Elle crée une nouvelle instance de Worker.
Le premier paramètre transmis au constructeur Worker est le chemin d’accès au fichier JavaScript à exécuter dans le worker. Dans cet exemple, vous pouvez utiliser la variable globale __filename, car le même fichier contient le code du thread principal et celui du thread worker. Le deuxième paramètre du thread Worker est un objet d’options. Sa propriété workerData vous permet de transmettre des valeurs au thread worker.
Le reste du code de la section du thread principal configure des écouteurs d’événements sur l’instance de Worker. Ils vous permettent de réagir aux messages, aux erreurs et à la fin du processus dans le worker.
Lorsque l’exemple de code est exécuté par un thread worker, la variable isMainThread vaut false et la branche else s’exécute. Elle utilise l’export workerData du module worker_threads pour accéder aux données transmises par le thread principal. L’export parentPort fournit une interface avec le thread principal, tandis que la fonction postMessage() permet de lui renvoyer des données. Celles-ci sont alors signalées sous la forme d’un événement message.
L’exécution de worker-demo.js entraîne donc la séquence d’actions suivante :
Un nouveau worker est créé et reçoit
hellocomme donnée.Le code exécuté dans le worker accède à ses données et renvoie un nouveau message amical au thread principal.
L’écouteur d’événements du thread principal récupère le message envoyé par le thread worker et l’affiche dans la console.
Le thread worker n’a plus de code à exécuter et se termine. Le thread principal en est informé par l’événement
exit.
Vous pouvez maintenant essayer quelques exemples concrets de threads worker.
Redimensionner des images avec des threads worker
Ce code convertit une image en trois tailles différentes en parallèle. C’est beaucoup plus rapide que de traiter chaque taille l’une après l’autre.
Commencez par créer le code qui redimensionnera l’image. Enregistrez-le dans resize-worker.js :
Créez le code du thread principal dans un nouveau fichier nommé resize-main.js :
Utilisez npm pour installer le module sharp, qui fournit la fonction de redimensionnement d’images :
Placez ensuite une grande image dans votre répertoire de travail et nommez-la image.jpg. Vous pouvez utiliser cette photo colorée de fils sur Unsplash. Exécutez votre code avec la commande suivante :
Les trois messages « Resizing » devraient s’afficher instantanément. Le thread principal parcourt les tailles demandées, crée un nouveau thread pour chacune et attend qu’ils aient terminé. Les différentes tailles sont ainsi générées en parallèle, sans bloquer le thread principal.
Redimensionner des vidéos avec des threads worker
Cet extrait de code montre comment redimensionner une vidéo dans un thread worker avec [FFmpeg. Le thread principal reste ainsi libre d’effectuer d’autres tâches pendant le redimensionnement, qui sollicite fortement le processeur.
Enregistrez le code du worker dans un nouveau fichier nommé video-worker.js :
Enregistrez maintenant le code du thread principal dans un nouveau fichier nommé video-main.js :
Installez les dépendances npm nécessaires au code :
Cet exemple utilise l’encodeur populaire ffmpeg. Le package fluent-ffmpeg est un wrapper Node.js qui s’appuie sur les bibliothèques ffmpeg déjà installées sur votre système. Vous devez [installer ffmpeg avant de pouvoir exécuter votre code. La plupart des environnements Linux l’incluent déjà, mais les utilisateurs de Windows et de Mac devront peut-être l’installer manuellement.
Pour vérifier si ffmpeg est disponible, exécutez la commande suivante :
Si le numéro de version s’affiche, ffmpeg est déjà installé. Si un message du type « command ‘ffmpeg’ not found » apparaît, suivez les instructions sur le site Web de ffmpeg pour télécharger la bibliothèque adaptée à votre système. Ffmpeg est également disponible dans les dépôts de paquets de la plupart des distributions Linux populaires, ainsi que dans Homebrew pour macOS :
Vous pouvez maintenant essayer de redimensionner une vidéo en arrière-plan. Si vous n’avez pas de fichier à votre disposition, vous pouvez télécharger ce clip sur Pexels. Enregistrez-le sous le nom video.mp4 dans votre répertoire de travail.
Exécutez ensuite votre script pour réduire la résolution du clip à 1280 x 720 :
Cet exemple fonctionne un peu différemment du précédent. Il s’appuie sur le concept de « pool de workers » pour améliorer l’efficacité. Contrairement à l’exemple précédent, il ne met pas automatiquement fin au processus une fois le redimensionnement terminé. Le thread worker reste actif et à l’écoute des messages : vous devrez donc l’arrêter en appuyant sur Ctrl+C dans votre terminal.
Les pools de workers peuvent contribuer à réduire la consommation de ressources. La création d’un worker entraîne une surcharge à chaque fois ; il est donc recommandé de lancer un nombre fixe de threads et de les réutiliser dans la mesure du possible. Poolifier, Piscina et d’autres bibliothèques similaires facilitent l’utilisation des threads worker de cette manière. Elles gèrent la création des threads, jusqu’à la limite définie par l’utilisateur, pour prendre en charge les nouvelles tâches. Les tâches suivantes sont ensuite attribuées aux threads existants dès qu’ils sont disponibles.
Chiffrer des fichiers avec des workers
Cet exemple montre comment associer worker_threads au module crypto de Node.js pour chiffrer un fichier dans un thread distinct.
Commencez par enregistrer le code du thread worker dans un nouveau fichier nommé encrypt-worker.js :
Copiez le code du thread principal dans un nouveau fichier nommé encrypt-main.js :
Créez un simple fichier texte dans votre répertoire de travail, prêt à être chiffré, en exécutant la commande suivante dans votre terminal :
Exécutez maintenant votre code pour chiffrer le fichier :
Dans cet exemple, le worker envoie un message pour indiquer à l’utilisateur le chemin du fichier de sortie et la clé secrète générée.
Les bases des threads worker
Node.js est un environnement d’exécution à thread unique. Cela vient du fait que JavaScript est un langage synchrone qui bloque l’exécution et exécute tout dans un seul thread. Les composants asynchrones de la bibliothèque standard de Node.js semblent ne pas bloquer l’exécution parce qu’ils utilisent un pool de threads distinct.
Le module worker_threads vous permet d’intégrer une solution similaire à votre propre code. Il ne s’agit pas de véritable multithreading, mais, dans de nombreux cas, le résultat s’en approche suffisamment pour vous permettre d’exécuter du code en parallèle, en dehors du thread principal.
Cet article vous a expliqué ce que sont les threads de travail, dans quels cas les utiliser et comment les intégrer à votre projet. Le prochain article de cette série examinera les avantages et les inconvénients des threads de travail et les comparera aux implémentations du multithreading dans d’autres langages de programmation.
