El lenguaje JavaScript es una de las maravillas del mundo del software. Es increíblemente potente, flexible y versátil. Sin embargo, una limitación de su diseño fundamental es su naturaleza de un solo subproceso. El JavaScript tradicional parece manejar tareas paralelas, pero eso es un truco de sintaxis. Para lograr un paralelismo verdadero, es necesario utilizar enfoques de subprocesos múltiples modernos, como trabajadores web e hilos de trabajo. Paralelismo vs. concurrenciaLa forma más básica de entender la diferencia entre paralelismo y concurrencia es que la concurrencia es semántica, mientras que el paralelismo es implementación. Lo que quiero decir es que la concurrencia le permite indicar al sistema (semántica) que haga más de una cosa a la vez. El paralelismo simplemente realiza múltiples tareas simultáneamente (implementación). Todo procesamiento paralelo es concurrente, pero no toda programación concurrente es paralela. En JavaScript puro, puedes indicarle a la plataforma que haga un par de cosas: function fetchPerson(id) { return new Promise((resolve, reject) => { fetch(`https://swapi.dev/api/people/${id}`) .then(response => response.json()) .then(data => resolve(data)) .catch(error => reject(error)); }); } const lukeId = 1; const leiaId = 5; console.log(«Obteniendo personajes de Star Wars…»); // Obtener datos de personajes simultáneamente (sin bloqueo) Promise.all([fetchPerson(lukeId), fetchPerson(leiaId)]) .then(data => { console.log(«Caracteres recibidos:»); console.log(data[0]); // Datos de Luke Skywalker (ID: 1) console.log(data[1]); // Datos de Leia Organa (ID: 5) }) .catch(error => console.error(«Error al obtener los personajes:», error)); console.log(«Pasando a otras cosas…»); // Obteniendo personajes de Star Wars… // Pasando a otras cosas… Personajes recibidos: {name: ‘Luke Skywalker’, height: ‘172’, mass: ’77’, …} {name: ‘Leia Organa’, height: ‘150’, mass: ’49’, …} Esto parece obtener datos sobre Luke y Leia al mismo tiempo, al usar Promise.all para ejecutar dos llamadas de obtención juntas. Sin embargo, en verdad, JavaScript programará cada tarea para que sea manejada por el hilo de la aplicación. Esto se debe a que JavaScript usa un bucle de eventos. El bucle selecciona cosas de una cola tan rápido que a menudo parece que sucede simultáneamente, pero no es un proceso verdaderamente simultáneo. Para hacer realmente dos cosas a la vez, necesitamos múltiples hilos. Los subprocesos son una abstracción de los procesos del sistema operativo subyacente y su acceso al hardware, incluidos los procesadores multinúcleo. Subprocesos múltiples con trabajadores web Los trabajadores web le brindan una forma de generar subprocesos en un navegador web. Puede simplemente cargar un script de trabajador separado del script principal y manejará los mensajes asincrónicos. Cada controlador de mensajes se ejecuta en su propio subproceso, lo que le brinda un verdadero paralelismo. Para nuestro ejemplo simple de API de Star Wars, queremos generar subprocesos que manejarán o recuperarán solicitudes. Usar trabajadores web para esto es excesivo, obviamente, pero mantiene las cosas simples. Queremos crear un trabajador web que acepte un mensaje del subproceso principal y emita las solicitudes. Así es como se ve ahora nuestro script principal (main.js): function fetchPersonWithWorker(id) { return new Promise((resolve, reject) => { const worker = new Worker(‘worker.js’); worker.onmessage = function(event) { if (event.data.error) { reject(event.data.error); } else { resolve(event.data); } worker.terminate(); // Limpia el trabajador después de recibir los datos } worker.postMessage({ url: `https://swapi.dev/api/people/${id}` }); }); } const lukeId = 1; const leiaId = 5; console.log(«Obteniendo personajes de Star Wars con web worker…»); // Obtener datos de personajes simultáneamente (verdaderamente en paralelo) Promise.all([fetchPersonWithWorker(lukeId), fetchPersonWithWorker(leiaId)]) .then(data => { console.log(«Caracteres recibidos:»); console.log(data[0]); // Datos de Luke Skywalker (ID: 1) console.log(data[1]); // Datos de Leia Organa (ID: 5) }) .catch(error => console.error(«Error al obtener caracteres:», error)); console.log(«Pasando a otras cosas…»); Esto es similar al primer ejemplo, pero en lugar de usar una función que funciona localmente en Promise.all, pasamos la función fetchPersonWithWorker. Esta última función crea un objeto Worker llamado worker, que se configura con el archivo worker.js. Una vez que se crea el objeto worker, proporcionamos un evento onmessage en él. Lo usaremos para manejar los mensajes que regresan del worker. En nuestro caso, resolvemos o rechazamos la promesa que estamos devolviendo (consumida por Promise.all en el script principal), luego finalizamos el worker. Después de eso, llamamos a worker.postMessage() y pasamos un objeto JSON simple con un campo URL establecido en la URL que queremos llamar. El web workerAquí está el otro lado de la ecuación, en worker.js: // worker.js onmessage = function(event) { console.log(“onmessage: “ + event.data); // {«url»: «https://swapi.dev/api/people/1»} const { url } = event.data; fetch(url) .then(response => response.json()) .then(data => postMessage(data)) .catch(error => postMessage({ error })); } Nuestro simple controlador onmessage acepta el evento y usa el campo URL para emitir las mismas llamadas de búsqueda que antes, pero esta vez usamos postMessage() para comunicar los resultados a main.js. Entonces, puedes ver que nos comunicamos entre los dos mundos con mensajes usando postMessage y onmessage. Recuerda: los controladores onmessage en el trabajador ocurren de manera asincrónica en sus propios subprocesos. (No utilice variables locales para almacenar datos, ya que es probable que se borren). Subprocesos del lado del servidor con subprocesos de trabajoAhora echemos un vistazo al lado del servidor, utilizando Node.js. En este caso, en lugar de trabajadores web, utilizamos el concepto de un subproceso de trabajo. Un subproceso de trabajo es similar a un trabajador web en el sentido de que pasamos mensajes de ida y vuelta desde el subproceso principal al trabajador. Por ejemplo, digamos que tenemos dos archivos, main.js y worker.js. Ejecutaremos main.js (usando el comando: node main.js) y generará un subproceso cargando worker.js como un subproceso de trabajo. Aquí está nuestro archivo main.js: const { Worker } = require(‘worker_threads’); función fetchPersonWithWorker(id) { return new Promise((resolve, reject) => { const worker = new Worker(‘./worker.js’, { workerData: id }); worker.on(‘message’, (data) => { if (data.error) { reject(data.error); } else { resolve(data); } worker.terminate(); }); worker.on(‘error’, (error) => reject(error)); let url = `https://swapi.dev/api/people/${id}`; worker.postMessage({ url }); }); } const lukeId = 1; const leiaId = 5; console.log(«Obteniendo personajes de Star Wars con subprocesos de trabajo…»); Promise.all([fetchPersonWithWorker(lukeId), fetchPersonWithWorker(leiaId)]) .then(data => { console.log(«Caracteres recibidos: «+ JSON.stringify(data) ); console.log(data[0]); // Datos de Luke Skywalker (ID: 1) console.log(data[1]); // Datos de Leia Organa (ID: 5) }) .catch(error => console.error(«Error al obtener caracteres:», error)); console.log(«Pasando a otras cosas…»); Importamos Worker desde el módulo worker_threads, pero tenga en cuenta que está integrado en Node, por lo que no necesitamos NPM para esto. Para iniciar el worker, creamos un nuevo objeto Worker y le damos el archivo worker.js como parámetro. Una vez hecho esto, agregamos un detector de mensajes que resuelve o rechaza nuestra promesa; esto es exactamente como lo hicimos para el trabajador web. También finalizamos el worker cuando termina, para limpiar los recursos. Finalmente, enviamos al worker un nuevo mensaje que contiene la URL que queremos recuperar. El hilo del workerA continuación, se muestra un vistazo a worker.js: const { parentPort } = require(‘worker_threads’); parentPort.on(‘message’, (msg) => { console.log(«message(worker): » + msg.url); fetch(msg.url) .then(response => response.json()) .then(data => parentPort.postMessage(data)) .catch(error => parentPort.postMessage({ error })); }); Nuevamente importamos desde worker_threads, esta vez el objeto parentPort. Este es un objeto que nos permite comunicarnos con el hilo principal. En nuestro caso, escuchamos el evento message, y cuando lo recibimos, desempaquetamos el campo url del mismo y lo usamos para emitir una petición. De esta manera hemos logrado peticiones verdaderamente concurrentes a las URLs. Si ejecutas el ejemplo con node main.js, verás los datos de ambas URLs en la salida a la consola. ConclusiónHas visto los mecanismos fundamentales para lograr hilos verdaderamente paralelos en JavaScript, tanto en el navegador como en el servidor. La forma en que se ejecutan depende del sistema operativo y del perfil de hardware del entorno de host real, pero en general, le brindan acceso a procesos multiproceso. Si bien JavaScript no admite el rango y la profundidad de la programación concurrente que se encuentra en un lenguaje como Java, los trabajadores web y los subprocesos de trabajo le brindan el mecanismo básico para el paralelismo cuando lo necesita. Puede encontrar los ejemplos ejecutables para este artículo en GitHub aquí. Para ejecutar el ejemplo de trabajador web, escriba $ node server.js desde el directorio raíz. Para ejecutar el ejemplo de subproceso de trabajo, escriba: ~/worker-thread $ node main.js Copyright © 2024 IDG Communications, Inc.