La práctica de trabajar con hilos en Node.js 10.5.0

Más recientemente, se lanzó la versión 10.5.0 de la plataforma Node.js. Una de sus características principales fue el soporte para trabajar con secuencias que se agregaron por primera vez a Node.js, sin dejar de ser experimental. Este hecho es especialmente interesante a la luz del hecho de que la plataforma ahora tiene esta oportunidad, cuyos seguidores siempre se han sentido orgullosos del hecho de que no necesita transmisiones debido al fantástico subsistema de E / S asíncrono. Sin embargo, el soporte de hilos ha aparecido en Node.js. ¿Por qué sería eso? ¿Para quién y por qué pueden ser útiles?



En pocas palabras, esto es necesario para que la plataforma Node.js pueda alcanzar nuevas alturas en aquellas áreas en las que anteriormente no mostraba los resultados más notables. Estamos hablando de realizar cálculos que utilizan los recursos del procesador de forma intensiva. Esta es principalmente la razón por la que Node.js no difiere en posiciones fuertes en áreas tales como inteligencia artificial, aprendizaje automático, procesamiento de grandes cantidades de datos. Se ha realizado un gran esfuerzo para permitir que Node.js se muestre bien en la resolución de tales problemas, pero aquí esta plataforma aún parece mucho más modesta que, por ejemplo, en el desarrollo de microservicios.

El autor del material, cuya traducción publicamos hoy, dice que decidió reducir la documentación técnica, que se puede encontrar en la solicitud de extracción original y en las fuentes oficiales , a un conjunto de ejemplos prácticos simples. Espera que cualquiera que vea estos ejemplos sepa lo suficiente como para comenzar a usar hilos en Node.js.

Acerca del módulo worker_threads y el indicador --experimental-worker


El soporte de worker_threads múltiples en Node.js se implementa como un módulo worker_threads . Por lo tanto, para aprovechar la nueva característica, este módulo debe conectarse utilizando el comando require .

Tenga en cuenta que solo puede trabajar con worker_threads utilizando el worker_threads - experimental-worker cuando ejecuta el script; de lo contrario, el sistema no encontrará este módulo.

Tenga en cuenta que la bandera incluye la palabra "trabajador", no "hilo". Exactamente de lo que estamos hablando se menciona en la documentación, que utiliza los términos "hilo de trabajo" (hilo de trabajo) o simplemente "trabajador" (trabajador). En el futuro, seguiremos el mismo enfoque.

Si ya ha escrito código multiproceso, entonces, explorando las nuevas características de Node.js, verá muchas cosas con las que ya está familiarizado. Si no ha trabajado con algo como esto antes, continúe leyendo más, ya que aquí se darán las explicaciones apropiadas para los recién llegados.

Acerca de las tareas que se pueden resolver con la ayuda de los trabajadores en Node.js


Los flujos de trabajo están destinados, como ya se mencionó, para resolver tareas que usan intensivamente las capacidades del procesador. Cabe señalar que su uso para resolver problemas de E / S es una pérdida de recursos, ya que, de acuerdo con la documentación oficial, los mecanismos internos de Node.js destinados a organizar E / S asíncronas son mucho más eficientes en sí mismos que el uso resolviendo el mismo problema de flujo de trabajadores. Por lo tanto, decidimos de inmediato que no trataremos con la entrada y salida de datos utilizando trabajadores.

Comencemos con un ejemplo simple que demuestra cómo crear y usar trabajadores.

Ejemplo no 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 salida de este código se verá como un conjunto de líneas que muestran contadores cuyos valores aumentan a diferentes velocidades.


Los resultados del primer ejemplo.

Nos ocuparemos de lo que está sucediendo aquí:

  1. Las instrucciones dentro de la expresión if crean 2 hilos, cuyo código, gracias al parámetro __filename , se toma del mismo script que Node.js pasó cuando se ejecutó el ejemplo. Ahora los trabajadores necesitan la ruta completa al archivo con el código, no admiten rutas relativas, por lo que este valor se usa aquí.
  2. Los datos a estos dos trabajadores se envían como un parámetro global, en forma del atributo workerData , que se utiliza en el segundo argumento. Después de eso, el acceso a este valor se puede obtener a través de una constante con el mismo nombre (preste atención a cómo se crea la constante correspondiente en la primera línea del archivo y cómo, en la última línea, se usa).

Aquí hay un ejemplo muy simple del uso del módulo worker_threads , nada interesante está sucediendo aquí todavía. Por lo tanto, considere otro ejemplo.

Ejemplo no 2


Considere un ejemplo en el que, en primer lugar, realizaremos algunos cálculos "pesados" y, en segundo lugar, haremos algo asíncrono en el hilo 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}); } 

Para ejecutar este ejemplo, preste atención al hecho de que este código necesita el módulo de request (se puede instalar usando npm, por ejemplo, usando los npm init --yes y npm install request --save en un directorio vacío con el archivo que contiene el código anterior npm install request --save ), y el hecho de que utiliza el módulo auxiliar, que está conectado por el comando const sorter = require("./list-sorter"); . El archivo de este módulo ( list-sorter.js ) debe estar en el mismo lugar que el archivo descrito anteriormente, su código se ve así:

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

Esta vez estamos resolviendo simultáneamente dos problemas. En primer lugar, cargamos la página de inicio de google.com y, en segundo lugar, clasificamos una matriz generada aleatoriamente de un millón de números. Esto puede demorar unos segundos, lo que nos brinda una gran oportunidad para ver los nuevos mecanismos Node.js en acción. Además, aquí medimos el tiempo que le toma al subproceso de trabajo ordenar los números, después de lo cual enviamos el resultado de la medición (junto con el primer elemento de la matriz ordenada) a la secuencia principal, que muestra los resultados en la consola.


El resultado del segundo ejemplo.

En este ejemplo, lo más importante es demostrar el mecanismo de intercambio de datos entre hilos.
Los trabajadores pueden recibir mensajes del hilo principal gracias al método on . En el código puede encontrar los eventos que estamos escuchando. El evento de message se message cada vez que enviamos un mensaje desde un determinado hilo utilizando el método parentPort.postMessage . Además, se puede usar el mismo método para enviar un mensaje a un hilo accediendo a una instancia de trabajador y recibirlos usando el objeto parentPort .

Ahora veamos otro ejemplo, muy similar a lo que ya hemos visto, pero esta vez prestaremos especial atención a la estructura del proyecto.

Ejemplo no 3


Como último ejemplo, proponemos considerar la implementación de la misma funcionalidad que en el ejemplo anterior, pero esta vez mejoraremos la estructura del código, lo haremos más limpio, lo llevaremos a una forma que mejore la conveniencia de apoyar un proyecto de software.

Aquí está el código para el programa 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}) //     }) 

Y aquí está el código que describe el comportamiento del subproceso de trabajo (en el programa anterior, la ruta al archivo con este código se forma usando la __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}); 

Estas son las características de este ejemplo:

  1. Ahora el código para el subproceso principal y para el subproceso de trabajo se encuentra en diferentes archivos. Esto facilita el apoyo y la expansión del proyecto.
  2. La función startWorker devuelve una nueva instancia del trabajador, que permite, si es necesario, enviar mensajes a este trabajador desde la transmisión principal.
  3. No es necesario verificar si el código se está ejecutando en el hilo principal (eliminamos la if con la verificación correspondiente).
  4. Se muestra un fragmento de código comentado en el trabajador, que demuestra el mecanismo para recibir mensajes del flujo principal, que, dado el mecanismo de envío de mensajes ya discutido, permite el intercambio de datos asincrónico bidireccional entre el flujo principal y el flujo de trabajo.

Resumen


En este artículo, nosotros, usando ejemplos prácticos, examinamos las características del uso de las nuevas capacidades para trabajar con flujos en Node.js. Si ha dominado lo que se discutió aquí, significa que está listo para ver su documentación y comenzar sus propios experimentos con el módulo worker_threads . Quizás valga la pena señalar que esta característica solo apareció en Node.js, aunque es experimental, por lo que con el tiempo, algo en su implementación puede cambiar. Además, si durante sus propios experimentos con worker_threads encuentra errores o encuentra que este módulo no interfiere con alguna característica que le falta, informe a los desarrolladores y ayude a mejorar la plataforma Node.js.

Estimados lectores! ¿Qué opinas del soporte de subprocesos múltiples en Node.js? ¿Planea utilizar esta función en sus proyectos?

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


All Articles