En versiones recientes de Node.js, comenzando con v10.5.0 y solidificándose aún más en Node.js v12 LTS, la introducción del módulo «worker_threads» ha transformado la forma en que abordamos la concurrencia en Node.js. Este módulo aporta capacidades de subprocesos múltiples a la plataforma, lo que permite a los desarrolladores realizar tareas que requieren un uso intensivo de la CPU de manera más eficiente. En este artículo, exploraremos la importancia de los subprocesos de trabajo, su contexto histórico y por qué son una valiosa adición al ecosistema Node.js. La evolución de la concurrencia en JavaScript y Node.js JavaScript, originalmente diseñado como un subproceso único lenguaje, inicialmente sirvió para mejorar la interactividad en las páginas web. Funcionó bien en esta función, ya que había una necesidad limitada de capacidades complejas de subprocesos múltiples en este contexto. Sin embargo, Ryan Dahl vio esta limitación como una oportunidad cuando creó Node.js. Quería implementar una plataforma del lado del servidor basada en E/S asíncrona para evitar la necesidad de subprocesos y hacer las cosas mucho más fáciles, pero la concurrencia puede ser un problema muy difícil de resolver. Tener muchos subprocesos accediendo a la misma memoria puede producir condiciones de carrera que son muy difíciles de reproducir y corregir. ¿Node.js es de un solo subproceso? En realidad, nuestras aplicaciones Node.js son de un solo subproceso. Podemos ejecutar cosas en paralelo, pero no creamos hilos ni los sincronizamos. La máquina virtual y el sistema operativo ejecutan la E/S en paralelo para nosotros, y cuando llega el momento de enviar datos a nuestro código JavaScript, es JavaScript el que se ejecuta en un solo hilo. En otras palabras, todo se ejecuta en paralelo excepto nuestro código JavaScript. Los bloques sincrónicos de código JavaScript siempre se ejecutan uno a la vez :let flag = false function doSomething() { flag = true // Más código (eso no cambia `flag`)… // Podemos estar seguros de que ` flag` aquí es cierto. // No hay forma de que otro bloque de código haya cambiado // `flag` ya que este bloque es sincrónico. }Esto es genial si todo lo que hacemos es E/S asíncrona. Nuestro código consta de pequeñas porciones de bloques sincrónicos que se ejecutan rápidamente y pasan datos a archivos y secuencias, por lo que nuestro código JavaScript es tan rápido que no bloquea la ejecución de otras partes de JavaScript. Se dedica mucho más tiempo a esperar. /O eventos que sucederán que el código JavaScript que se está ejecutando. Veamos esto con un ejemplo rápido: db.findOne(‘SELECT… LIMIT 1’, function(err, result) { if (err) return console.error(err) console.log(resultado) }) console.log (‘Consulta en ejecución’) setTimeout(function() { console.log(‘Hola’) }, 1000)Tal vez esta consulta de base de datos demore un minuto, pero el mensaje «Consulta en ejecución» se mostrará inmediatamente después de invocar la consulta. Y veremos el mensaje «Hola» un segundo después de invocar la consulta, independientemente de si la consulta todavía se está ejecutando o no. Nuestra aplicación Node.js simplemente invoca la función y no bloquea la ejecución de otras piezas de código. Se le notificará a través de la devolución de llamada cuando finalice la consulta y recibiremos el resultado. Desafíos de las tareas que requieren un uso intensivo de la CPU Sin embargo, la naturaleza de un solo subproceso de JavaScript plantea desafíos cuando se trata de tareas vinculadas a la CPU que requieren cálculos intensivos en grandes cantidades. conjuntos de datos. En tales casos, la ejecución sincrónica de código puede convertirse en un cuello de botella, lo que lleva a un rendimiento lento y al bloqueo de otras tareas críticas. La búsqueda de subprocesos múltiples en JavaScript La pregunta natural que surge es si podemos introducir subprocesos múltiples en JavaScript para abordar las tareas vinculadas a la CPU de manera más efectiva. Desafortunadamente, no es una tarea sencilla. Agregar subprocesos múltiples a JavaScript requeriría cambios fundamentales en el lenguaje mismo. Los lenguajes que admiten subprocesos múltiples suelen tener construcciones especializadas, como palabras clave de sincronización, para permitir que los subprocesos cooperen sin problemas. La solución simple pero imperfecta: división de código sincrónica Para abordar los desafíos que plantean las tareas vinculadas a la CPU, los desarrolladores han recurrido a una técnica conocida como código sincrónico. terrible. Este enfoque implica dividir tareas complejas en bloques de código sincrónico más pequeños y usar «setImmediate(callback)» para permitir que otras tareas pendientes se procesen en el medio. Si bien es una solución viable en algunos casos, tiene limitaciones y puede complicar la estructura del código, especialmente para algoritmos más complejos.// Ejemplo de uso de setImmediate() para dividir código const crypto = require(‘crypto’) const arr = new Array( 200).fill(‘algo’) function processChunk() { if (arr.length === 0) { // Código que se ejecuta después de ejecutar toda la matriz } else { console.log(‘Procesando fragmento’); const subarr = arr.splice(0, 10) for (const item of subarr) { doHeavyStuff(item) } setImmediate(processChunk) } } ProcessChunk()Ejecución de procesos paralelos sin subprocesosAfortunadamente, existe un enfoque alternativo al procesamiento paralelo que no confiar en hilos tradicionales. Al aprovechar módulos como «worker-farm», los desarrolladores pueden lograr paralelismo bifurcando procesos y gestionando la distribución de tareas de manera efectiva. Este método permite que la aplicación principal se comunique con procesos secundarios a través del paso de mensajes basado en eventos, evitando problemas de memoria compartida y condiciones de carrera.// Ejemplo de ejecución de procesos paralelos usando work-farm const trabajadorFarm = require(‘worker-farm’) const service = trabajadorFarm(require.resolve(‘./script’)) service(‘hello’, function (err, output) { console.log(output) })Introducción de subprocesos de trabajoLa introducción de subprocesos de trabajo en Node.js proporciona una forma elegante y solución eficiente a los desafíos que plantean las tareas vinculadas a la CPU. Los subprocesos de trabajo ofrecen contextos aislados, cada uno con su propio entorno JavaScript. Se comunican con el proceso principal a través del paso de mensajes, lo que garantiza que no haya condiciones de carrera ni problemas de memoria compartida. Como dice la documentación, “los trabajadores son útiles para realizar operaciones de JavaScript que requieren un uso intensivo de la CPU; no los use para E/S, ya que los mecanismos integrados de Node.js para realizar operaciones de forma asincrónica ya lo tratan de manera más eficiente que los subprocesos de trabajo”. Los subprocesos de trabajo son más livianos que el paralelismo que puede obtener usando child_process o cluster. Además, Workers_threads puede compartir memoria de manera eficiente. // Ejemplo de uso de Worker Threads const { Worker } = require(‘worker_threads’) function runService(workerData) { return new Promise((resolve, rechazar) => { const trabajador = new Worker( ‘./service.js’, { trabajadorData }) trabajador.on(‘mensaje’, resolver) trabajador.on(‘error’, rechazar) trabajador.on(‘salir’, (código) => { if (código! == 0) rechazar(nuevo Error(`Trabajador detenido con código de salida ${code}`)) }) }) } función asíncrona ejecutar() { resultado constante = esperar runService(‘mundo’) console.log(resultado) } run().catch(err => console.error(err)) Uso de subprocesos de trabajo para múltiples tareas A partir de Node.js v10.5.0, los desarrolladores pueden aprovechar fácilmente el poder de los subprocesos de trabajo. Para versiones anteriores a Node.js 11.7.0, habilitar subprocesos de trabajo requiere el indicador “–experimental-worker”. Los desarrolladores pueden crear un grupo de subprocesos de trabajo para optimizar la utilización de recursos, asegurando que se puedan ejecutar múltiples tareas en paralelo mientras se conserva la memoria. Digamos que estamos creando una aplicación que permite a los usuarios cargar una imagen de perfil y luego generar múltiples tamaños a partir de la imagen original (por ejemplo: 50 x 50 o 100 x 100) para los diferentes casos de uso dentro de la aplicación. El procedimiento de cambiar el tamaño de la imagen consume mucha CPU y tener que cambiar su tamaño en diferentes tamaños bloquearía el hilo principal. Esta tarea de cambiar el tamaño de la imagen se puede asignar al subproceso de trabajo, mientras que el subproceso principal maneja otras tareas ingrávidas. Los subprocesos de trabajo en js son útiles en estos casos: Algoritmos de búsqueda. Ordenar una gran cantidad de datos. Compresión de video. Cambio de tamaño de imagen. Factorización de números grandes. Generación de números primos en un rango determinado. Conclusión Con el lanzamiento estable del módulo “worker_threads” en Node.js v12 LTS, los desarrolladores han obtenido una solución sólida para manejar tareas que requieren un uso intensivo de la CPU en aplicaciones Node.js. Ya sea que esté optimizando el rendimiento del lado del servidor o mejorando la experiencia del usuario en aplicaciones web, los subprocesos de trabajo ofrecen una herramienta poderosa para lograr el paralelismo sin las complejidades del subproceso múltiple tradicional.