La pratique de travailler avec des threads dans Node.js 10.5.0

Plus récemment, la version 10.5.0 de la plateforme Node.js est sortie. L'une de ses principales caractéristiques était la prise en charge du travail avec les flux qui avait été ajoutée pour la première fois à Node.js, tout en restant expérimentale. Ce fait est particulièrement intéressant à la lumière du fait que la plate-forme a maintenant cette opportunité, dont les adhérents ont toujours été fiers du fait qu'elle n'a pas besoin de flux en raison du fantastique sous-système d'E / S asynchrones. Cependant, le support des threads est apparu dans Node.js. Pourquoi serait-ce? À qui et pourquoi peuvent-ils être utiles?



En résumé, cela est nécessaire pour que la plateforme Node.js puisse atteindre de nouveaux sommets dans les domaines où elle ne montrait pas auparavant les résultats les plus remarquables. Nous parlons d'effectuer des calculs qui utilisent intensivement les ressources du processeur. C'est principalement la raison pour laquelle Node.js n'est pas très performant dans des domaines tels que l'intelligence artificielle, l'apprentissage automatique et le traitement de grandes quantités de données. Beaucoup d'efforts ont été déployés pour permettre à Node.js de bien se montrer dans la résolution de tels problèmes, mais ici cette plate-forme semble encore beaucoup plus modeste que, par exemple, dans le développement de microservices.

L'auteur du document, dont nous publions aujourd'hui la traduction, a déclaré qu'il avait décidé de réduire la documentation technique, qui se trouve dans la demande de tirage d' origine et dans les sources officielles , à un ensemble d'exemples pratiques simples. Il espère que quiconque regarde ces exemples en saura suffisamment pour démarrer avec les threads dans Node.js.

À propos du module worker_threads et de l'indicateur --experimental-worker


La prise en charge du multithreading dans Node.js est implémentée en tant que module worker_threads . Par conséquent, afin de profiter de la nouvelle fonctionnalité, ce module doit être connecté à l'aide de la commande require .

Notez que vous ne pouvez travailler avec worker_threads utilisant l' worker_threads - experimental-worker lors de l'exécution du script, sinon le système ne trouvera pas ce module.

Notez que le drapeau comprend le mot «travailleur», pas «thread». Exactement ce dont nous parlons est mentionné dans la documentation, qui utilise les termes «thread de travail» (thread de travail) ou simplement «travailleur» (travailleur). À l'avenir, nous suivrons la même approche.

Si vous avez déjà écrit du code multithread, alors, en explorant les nouvelles fonctionnalités de Node.js, vous verrez beaucoup de choses que vous connaissez déjà. Si vous n'avez jamais travaillé avec quelque chose comme ça auparavant, continuez à lire, car les explications appropriées pour les nouveaux arrivants seront données ici.

À propos des tâches pouvant être résolues avec l'aide de travailleurs dans Node.js


Les flux de travail sont destinés, comme déjà mentionné, à résoudre des tâches qui utilisent intensivement les capacités du processeur. Il convient de noter que leur utilisation pour résoudre les problèmes d'E / S est un gaspillage de ressources, car, selon la documentation officielle, les mécanismes internes de Node.js visant à organiser les E / S asynchrones sont beaucoup plus efficaces en eux-mêmes que l'utilisation résoudre le même problème de flux de travailleurs. Par conséquent, nous décidons immédiatement que nous ne traiterons pas l'entrée et la sortie de données à l'aide de travailleurs.

Commençons par un exemple simple qui montre comment créer et utiliser des travailleurs.

Exemple n ° 1


 const { Worker, isMainThread,  workerData } = require('worker_threads'); let currentVal = 0; let intervals = [100,1000, 500] function counter(id, i){   console.log("[", id, "]", i)   return i; } if(isMainThread) {   console.log("this is the main thread")   for(let i = 0; i < 2; i++) {       let w = new Worker(__filename, {workerData: i});   }   setInterval((a) => currentVal = counter(a,currentVal + 1), intervals[2], "MainThread"); } else {   console.log("this isn't")   setInterval((a) => currentVal = counter(a,currentVal + 1), intervals[workerData], workerData); } 

La sortie de ce code ressemblera à un ensemble de lignes montrant des compteurs dont les valeurs augmentent à différentes vitesses.


Les résultats du premier exemple

Nous allons traiter de ce qui se passe ici:

  1. Les instructions à l'intérieur de l'expression if créent 2 threads, dont le code, grâce au paramètre __filename , est extrait du même script que Node.js a transmis lors de l'exécution de l'exemple. Désormais, les utilisateurs ont besoin du chemin d'accès complet au fichier avec le code, ils ne prennent pas en charge les chemins relatifs, c'est pourquoi cette valeur est utilisée ici.
  2. Les données à ces deux travailleurs sont envoyées en tant que paramètre global, sous la forme de l'attribut workerData , qui est utilisé dans le deuxième argument. Après cela, l'accès à cette valeur peut être obtenu via une constante du même nom (faites attention à la façon dont la constante correspondante est créée dans la première ligne du fichier et comment, à la dernière ligne, elle est utilisée).

Voici un exemple très simple d'utilisation du module worker_threads , rien d'intéressant ne se passe encore ici. Par conséquent, considérons un autre exemple.

Exemple n ° 2


Prenons un exemple dans lequel, premièrement, nous effectuerons des calculs "lourds", et deuxièmement, ferons quelque chose d'asynchrone dans le thread principal.

 const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); const request = require("request"); if(isMainThread) {   console.log("This is the main thread")   let w = new Worker(__filename, {workerData: null});   w.on('message', (msg) => { //  !       console.log("First value is: ", msg.val);       console.log("Took: ", (msg.timeDiff / 1000), " seconds");   })   w.on('error', console.error);   w.on('exit', (code) => {       if(code != 0)           console.error(new Error(`Worker stopped with exit code ${code}`))   });   request.get('http://www.google.com', (err, resp) => {       if(err) {           return console.error(err);       }       console.log("Total bytes received: ", resp.body.length);   }) } else { //    function random(min, max) {       return Math.random() * (max - min) + min   }   const sorter = require("./list-sorter");   const start = Date.now()   let bigList = Array(1000000).fill().map( (_) => random(1,10000))   sorter.sort(bigList);   parentPort.postMessage({ val: sorter.firstValue, timeDiff: Date.now() - start}); } 

Pour exécuter cet exemple, faites attention au fait que ce code a besoin du module de request (il peut être installé en utilisant npm, par exemple, en utilisant les commandes npm init --yes et npm install request --save dans un répertoire vide avec le fichier contenant le code ci-dessus) npm install request --save ), et le fait qu'il utilise le module auxiliaire, qui est connecté par la commande const sorter = require("./list-sorter"); . Le fichier de ce module ( list-sorter.js ) doit être au même endroit que le fichier décrit ci-dessus, son code ressemble à ceci:

 module.exports = {   firstValue: null,   sort: function(list) {       let sorted = list.sort();       this.firstValue = sorted[0]   } } 

Cette fois, nous résolvons simultanément deux problèmes. Premièrement, nous chargeons la page d'accueil google.com, et deuxièmement, nous trions un tableau généré de manière aléatoire d'un million de numéros. Cela peut prendre quelques secondes, ce qui nous donne une excellente occasion de voir les nouveaux mécanismes Node.js en action. De plus, ici, nous mesurons le temps nécessaire au thread de travail pour trier les nombres, après quoi nous envoyons le résultat de la mesure (avec le premier élément du tableau trié) au flux principal, qui affiche les résultats dans la console.


Le résultat du deuxième exemple

Dans cet exemple, la chose la plus importante est de démontrer le mécanisme d'échange de données entre les threads.
Les travailleurs peuvent recevoir des messages du thread principal grâce à la méthode on. Dans le code, vous pouvez trouver les événements que nous écoutons. L'événement de message est message chaque fois que nous envoyons un message à partir d'un certain thread à l'aide de la méthode parentPort.postMessage . De plus, la même méthode peut être utilisée pour envoyer un message à un thread en accédant à une instance de travail et les recevoir à l'aide de l'objet parentPort .

Examinons maintenant un autre exemple, très similaire à ce que nous avons déjà vu, mais cette fois, nous porterons une attention particulière à la structure du projet.

Exemple n ° 3


Comme dernier exemple, nous proposons d'envisager la mise en œuvre de la même fonctionnalité que dans l'exemple précédent, mais cette fois, nous allons améliorer la structure du code, le rendre plus propre, l'amener sous une forme qui améliore la commodité de la prise en charge d'un projet logiciel.

Voici le code du programme principal.

 const { Worker, isMainThread, parentPort, workerData } = require('worker_threads'); const request = require("request"); function startWorker(path, cb) {   let w = new Worker(path, {workerData: null});   w.on('message', (msg) => {       cb(null, msg)   })   w.on('error', cb);   w.on('exit', (code) => {       if(code != 0)           console.error(new Error(`Worker stopped with exit code ${code}`))  });   return w; } console.log("this is the main thread") let myWorker = startWorker(__dirname + '/workerCode.js', (err, result) => {   if(err) return console.error(err);   console.log("[[Heavy computation function finished]]")   console.log("First value is: ", result.val);   console.log("Took: ", (result.timeDiff / 1000), " seconds"); }) const start = Date.now(); request.get('http://www.google.com', (err, resp) => {   if(err) {       return console.error(err);   }   console.log("Total bytes received: ", resp.body.length);   //myWorker.postMessage({finished: true, timeDiff: Date.now() - start}) //     }) 

Et voici le code qui décrit le comportement du thread de travail (dans le programme ci-dessus, le chemin d'accès au fichier avec ce code est formé à l'aide de la construction __dirname + '/workerCode.js' ):

 const {  parentPort } = require('worker_threads'); function random(min, max) {   return Math.random() * (max - min) + min } const sorter = require("./list-sorter"); const start = Date.now() let bigList = Array(1000000).fill().map( (_) => random(1,10000)) /** //      : parentPort.on('message', (msg) => {   console.log("Main thread finished on: ", (msg.timeDiff / 1000), " seconds..."); }) */ sorter.sort(bigList); parentPort.postMessage({ val: sorter.firstValue, timeDiff: Date.now() - start}); 

Voici les caractéristiques de cet exemple:

  1. Maintenant, le code du thread principal et du thread de travail se trouve dans différents fichiers. Cela facilite le soutien et l'expansion du projet.
  2. La fonction startWorker renvoie une nouvelle instance du travailleur, qui permet, si nécessaire, d'envoyer des messages à ce travailleur à partir du flux principal.
  3. Il n'est pas nécessaire de vérifier si le code s'exécute dans le thread principal (nous avons supprimé l' if avec la vérification correspondante).
  4. Le travailleur affiche un fragment de code commenté montrant le mécanisme de réception des messages du flux principal, qui, compte tenu du mécanisme d'envoi de messages déjà discuté, permet un échange de données asynchrone bidirectionnel entre le flux principal et le flux de travail.

Résumé


Dans cet article, nous, à l'aide d'exemples pratiques, avons examiné les fonctionnalités de l'utilisation des nouvelles fonctionnalités pour travailler avec des flux dans Node.js. Si vous maîtrisez ce qui a été discuté ici, cela signifie que vous êtes prêt à voir votre documentation et à démarrer vos propres expériences avec le module worker_threads . Il vaut peut-être la peine de noter que cette fonctionnalité n’est apparue que dans Node.js, bien qu’elle soit expérimentale. Au fil du temps, quelque chose dans son implémentation peut changer. De plus, si au cours de vos propres expériences avec worker_threads vous rencontrez des erreurs ou constatez que ce module n'interfère pas avec certaines fonctionnalités manquantes, informez-en les développeurs et aidez à améliorer la plateforme Node.js.

Chers lecteurs! Que pensez-vous de la prise en charge du multithreading dans Node.js? Envisagez-vous d'utiliser cette fonctionnalité dans vos projets?

Source: https://habr.com/ru/post/fr415659/


All Articles