Web Worker: ejecuta subprocesos JavaScript en paralelo

JavaScript, siempre ha sido un lenguaje que ha sido sinónimo de poco fiable a la hora de procesar grandes cantidades de datos o realizar procesos algo más complejos. Pero HTML5 y la mejora de los motores de JavaScript han demostrado que esto ya no es así. Gracias a la API de HTML5 de Web Worker podemos crear de una forma muy simple uno o varios subprocesos en nuestro hilo de ejecución, evitando esa saturación que algunas veces muestra una web cuando está procesando algo grande (lo que conlleva que se pille la web) gracias a que se ejecutan de forma asíncrona y en procesos diferentes.

A lo largo del artículo veremos cómo hacer el uso básico de la API, cómo paralelizar procesos, cómo se comporta según los navegadores y cómo afecta al propio sistema.

Esta API funciona de una forma muy similar a AJAX, ya que para ejecutar un Web Worker hay que llamar a un archivo externo, enviarle un mensaje y esperar su respuesta, todo de forma asíncrona, sin ralentizar la web en el proceso ni molestar de ninguna otra forma al usuario. De hecho se podría hacer la misma función con AJAX si detrás de la petición hubiese una respuesta por parte de nuestro servidor tras procesar ciertos datos.

Bueno, tras la comparación con AJAX (que no viene al caso, pero que la expongo para aclarar su funcionamiento) nos ponemos con la sencillísima API de Web Worker.

API de Web Worker

En el uso de la API intervienen dos archivos: el principal, donde creamos el Worker y el propio Worker, que es un archivo distinto. Para hacer el Worker debemos de crear un objeto Worker(), al que le pasaremos la ruta del archivo, posteriormente este nos dará la opción mediante postMessage() de mandar datos de cualquier tipo al Worker y a su vez también podremos "matar" el subproceso con terminate().

// Objeto Worker
var worker = new Worker('script/worker.js'); // Ruta del archivo JS
  
// Permite mandar mensajes al Worker
worker.postMessage("Hola Mundo!");
worker.postMessage(100);
worker.postMessage({status:1,error:['ping','pong']});
  
// Termina la ejecución del Worker (esté en el estado que esté)
worker.terminate();

Ahora creamos la respuesta ante un mensaje desde el Worker (cuando esté ejecutando y decida enviar un mensaje). Para acceder al evento realizaremos una escucha al evento "message", que lanza el objeto Worker().

// Al recibir un mensaje
worker.addEventListener('message', function(e) {
    // Salta la alerta
    alert('Esta es la respuesta del Worker: ' + e.data);
}, false);

En este caso, como podéis observar, el evento (e) contiene data, que nos proporciona los datos enviados por el mensaje desde el Worker.

El Worker

Ahora, para programar el Worker, debemos de crear un archivo nuevo, donde haremos prácticamente lo mismo, pero en esta ocasión la unión con la API se hace a través del objeto self. Al ser prácticamente idéntico solo comentaré el código y le enviaremos al Worker lo siguiente: [2,3].

// Al recibir un mensaje se ejecuta
self.addEventListener('message', function(e) {
    var a = e.data[0],
        b = e.data[1],
        r = a + b;  // Sumamos ambos datos
    self.postMessage( r );  // Contestamos con la suma
}, false);

Este es un ejemplo básico, en el que perderíamos más tiempo de ejecución creando el Worker y mandando los mensajes que ejecutando el propio script, pero en algo "más grande" nos aseguraríamos de no saturar la página principal.

Paralelización

Paralelizar estos subprocesos es tan sencillo como crear dos a la vez (volviendo a la comparación con AJAX, sería similar a pedir dos archivos), pero quiero poner un ejemplo más claro de procesamiento.

Actualmente con JavaScript se puede procesar o analizar audio y vídeo, generar escenas 2D y 3D o simplemente hallar mediante una fórmula una serie de número, que es en el ejemplo que nos vamos a centrar. Voy a coger como referencia un ejercicio de Solveet, que consiste en encontrar la lista de números narcisistas, que se podrían definir así:

Un número de N dígitos es narcisista si la suma de las potencias N-ésimas de sus dígitos es él mismo. Dos ejemplos serían los siguientes: 371 = 33 + 73 + 13 = 27 + 343 + 1 = 371 9474 = 94 + 44 + 74 + 44 = 6561 + 256 + 2401 + 256 =9474

Para probar los Web Worker vamos a paralelizar la búsqueda de los números narcisistas, sobre una implementación que buscará sobre todos y cada uno de los números que tengas N cifras. El script no estará optimizado para hacer la explicación más sencilla.

Primero lo que haremos será crear un bucle que genere tantos Workers como queramos, y a cada uno de ellos le asignaremos un rango de números en los que buscar.

function paralelo(workers){
      
    var maximo = Math.pow(10,n),    // Número máximo de dígitos
        parte = max/workers,        // Rango de números para cada Worker
        WKS = [],           // Contiene los Workers
        procesar = workers,     // Número de Workers
        N = 7,              // N dígitos del número narcisista
        time = (new Date()).getTime();  // Comienzo del script
  
    for(var i=0;i

Este sería un ejemplo que solo cuantificaría el tiempo en ejecutarse, para ello variaremos el número de Workers, asi veremos la diferencia entre paralelizar con JavaScript (usando un solo Worker no se crea la paralelización) y usar varios (el número perfecto es el de los núcleos de la CPU, ya sean lógicos o físicos, ya que usará el 100% de los recursos).

Ahora desde el Worker procesaríamos el rango de números en busca de esos números que son narcisistas.

self.addEventListener('message', function(e) {
  
    var min = e.data.first, // Inicio
        max = e.data.last;  // Fin
  
    for(var n=min;n

En el siguiente ejemplo podéis cambiar el número de Workers, así veréis las mejoras al paralelizar el proceso en JavaScript (que mejoraría más si optimizásemos el algoritmo, pero ahora estamos a el Worker).

Comportamiento por navegador

Todos los navegadores modernos tienen un buen soporte respetando el estándar, pero cada uno tiene sus peculiaridades a la hora de ejecutarlo. Pero no tenemos de qué preocuparnos, funcionarán perfectamente nuestros subprocesos.

Para realizar las pruebas para hallar las siguientes conclusiones usaré la siguiente implementación del ejercicio (podéis ver el código en GitHub), que permite establecer cualquier número de Workers, lo que me permite mostrar los límites de cada navegador.

  Chrome Firefox IE
N = 8
1 Worker
18,88 78,24 52,13
N = 8
4 Worker
9,93 43,55 28,48
Número máximo de Workers 110 20 25
Observaciones al usar 200 Workers Error en la página
("revienta la página")
No pasa de 20 No pasa de 25
Observaciones al usar 100 Workers Aumenta drásticamente
el uso de la RAM
No pasa de 20 No pasa de 25
Observaciones al usar 50 Workers Funciona correctamente No pasa de 20 No pasa de 25
Observaciones al usar 25 Workers Funciona correctamente No pasa de 20 Funciona correctamente
Observaciones al usar 20 Workers Funciona correctamente Aumenta drásticamente
el uso de la RAM
Funciona correctamente
Observaciones al usar 10 Workers Funciona correctamente Funciona correctamente Funciona correctamente

Estos datos son necesarios a la hora de gestionar los Workers cuando procesemos muchas cosas distintas, ya que podemos encontrarnos con las limitaciones de IE o Firefox (este último se me ha llegado a cerrar en alguna ocasión probando 20 Workers) o "reventando" el navegador si nos pasamos con Chrome.

Comportamiento en el sistema

Por último voy a mostraros cómo se comporta el sistema ante los Workers, y ver porqué no tenemos que pasarnos con los Workers. Las pruebas las realizaré con Chrome (IE es muy estable gracias a su límite y Firefox no aguanta muchos Workers ni con el límite). La prueba es con 100 Workers.

Este dato lo expongo como curiosidad, ya que los subprocesos que genere el navegador son irrelevantes, mientras funcionen correctamente, pero el siguiente dato es más interesante.

Este dato si es revelador, ya que con 100 Workers se produce una alta saturación (al igual que pasa con Firefox con 20) y debemos de ser cautos y no generar por error una infinidad de Workers.

Resumen

Los Web Workers le darán un gran plus de estabilidad y rapidez a una web de una forma muy simple, pero debemos de saber usarlos y sobre todo terminar su ejecución de forma correcta (con terminate()), sino produciremos un pequeño colapso en los recursos del sistema, aunque apuesto por que en futuras versiones de estos navegadores se mejore la implementación de la API.

Vía | Xitrus Código | GitHub Más información | W3C (Especificación)

Ver todos los comentarios en https://www.genbeta.com

VER 0 Comentario

Portada de Genbeta