Web Workers en HTML5. Paseo Aleatorio

Ver Demo

Una de las novedades de HTML5 es la posibilidad de crear hilos de ejecución para ejecutar tareas en segundo plano con el fin de no entorpecer la interacción del usario con la página. De este modo acciones que requieran un gran tiempo de procesamiento, especialmente en dispositivos con poca capacidad de proceso, y que bloquean la interface (más de 250ms), pueden ser ejecutadas en segundo plano. Estos hilos han sido denominados Web Workers, y como todo lo de HTML5, se programan usando Javascript. De momento sólo funcionan en las versiones más actuales de Firefox y Google Chrome. El código de este ejemplo está creado usando Google Chrome 20+.

Para usar un WebWorker simplemente hay que instanciarlo en el código JavaScript:

var worker = new WebWorker ("hilo1.js");

Como se puede ver, recibe por parámetro un fichero de Javascript, que contiene el código que se va a ejecutar cuando se lance el hilo. Para ejecutar un hilo simplemente hay que enviarle un mensaje que será captado mediante un listener para ese evento como veremos después.

worker.sendMessage();

Dentro del código del hilo (hilo1.js) está lo necesario para responder a esta llamada, devolviendo otro mensaje.

self.addEventListener('message',function(e)
{
 self.postMessage('fin');
});

Finalmente el hilo principal también registra un listener para “escuchar” los mensajes del web worker:

 worker.addEventListener('message',function(e){
 alert(e.data); //pinta fin en una ventana de alert
 });

El código del WebWorker es Javascript y tiene una serie de limitaciones muy importantes que determinan la arquitectura a la hora de usarlos. No tienen acceso a:

  • document
  • parent
  • window
  • otras librerías como jQuery o Prototype

Con estas limitaciones queda claro que dentro de los hilos no se puede hacer cualquier cosa ni pueden operar de forma independiente sin la interacción con el programa princopal. Realmente los hilos deberían limitarse a recibir y devolver información de tipo texto o JSON al hilo principal, de modo que sea éste el que haga las operaciones de manipulación de componentes del DOM. Por ejemplo, un caso de interacción típico sería:

  1. El hilo principal instancia un WebWorker para que haga una llamada AJAX y le devuelva la información en JSON. Esta información la mostrará en una capa DIV. Si lo hace el hilo principal tarda 1000ms debido al tamaño del JSON (o más en un ordenador/dispositivo lento).
  2. El WebWorker contiene en su código un “EventListener” donde está progrmaado el procesamiento de la acción indicada en el punto 1. Esta acción se ejecutará cuando la pida el hilo principal.
  3. El hilo principal llama al WebWorker y en éste se ejecuta el evento, lanzando la petición AJAX, que recibe una gran cantidad de texto en formato JSON. Parsea esta información a un objeto (y esta operación le lleva bastante tiempo).
  4. El WebWorker notifica al hilo principal que ha acabado, pasándole por parámetro el objeto JSON ya procesado.
  5. El hilo principal recive el evento de ese WebWorker y dispara una función que printa el objeto JSON en el DIV (algo muy rápido una vez se tiene el JSON).

De forma genérica se podría decir que los pasos que tiene que seguir una aplicación que use Web Workers debería ser:

  1. Hilo principal instancia el WebWorker y le ordena comenzar.
  2. El WebWorker tiene un EventListener que se activa cuando el hilo principal le ordena comenzar. En esta llamada se puede incluir información como un texto o un JSON.
  3. El WebWorker procesa y hace una llamada al hilo principal.
  4. El hilo principal tiene otro EventListener que se activa cuando el WebWorker termina. En esta llamada se puede incluir información como un texto o un JSON.

En nuestro ejemplo lo que intentamos programar es un programa que calcule caminos aleatorios (http://es.wikipedia.org/wiki/Camino_aleatorio) partiendo de un valor dado (100). Cada camino tiene 100 valores que son calculados por los hilos que lanzamos, y su resultado será pintado en un objeto canvas.

Un camino aleatorio parte de un punto, que en nuestro caso será el valor 100, y va generando, mediante números aleatorios diferentes valores basados en el paso anterior, así, partiendo del valor 100, el siguiente paso puede ser 101, el siguiente 104, el siguiente 102, luego 98… etcétera. Esto generará una gráfica con forma de rayo. Se trata simplemente de un proceso que puede ser computacionalmente costoso y que si se llevara a cabo en primer plano bloquearía la interfaz de usuario. Los hilos generarán estas series de 100 valores por cada camino en segundo plano y lo enviarán en formato JSON al hilo principal para que simplemente los pinte en el canvas.

En primer lugar vamos a analizar qué necesitamos:

  1. Un worker que genere series de 100 valores que representan caminos aleatorios y que envía al hilo principal en formato JSON
  2. Un generador de caminos aleatorios, incluyendo una función básica como es un generador de números aleatorios.
  3. Un listener en el hilo principal que se encarga de pintar el camino recibido en formato JSON en un canvas.
  4. Un control para parar el proceso del hilo cuando se desee.

Vamos por partes. En primer lugar tenemos que construir el listener para lanzar el evento de paso de mensaje. Hasta que este evento no se produce no hay lanzamiento del hilo:

self.addEventListener('message',function(e)
{
 var iteraciones = e.data.iteraciones;
 for(w=0;w<iteraciones;w++)
 {
 if (!stop)
 main(e);
 else
 {
 break;
 self.close();
 }
 }
});

Hay que notar un par de cosas:
1. Que existe una variable – global al worker – llamada “stop” con un valor booleano. Será modificada por otro evento para indicar que se debe para el hilo (de ahí el break y el self.close(), que cierra el bucle y termina el hilo).
2. Los parámetros que se le pasan al web worker, que será mediante JSON. En este caso se indican en el campo iteraciones el número de paseos aleatorios que va a generar cada hilo. Se lee mediante e.data.iteraciones.

Una vez tenemos el número de iteraciones se llama a la función principal, main(e) que genera un camino aleatorio y lo envía al hilo principal:

function main(e)
{
 var volatilidad = e.data.volatilidad;
 var media = e.data.media;
 var base = e.data.base;

var salida = "[";
 for(j=0;j<100;j++) //series de 100 valores
 {
 salida+="{\"V\":"+base+"},"; //compone la salida en array de JSON
 base=(siguiente_paso(media, volatilidad, base));
 }
 salida = salida.substring(0,salida.length-1);
 salida +="]";
 self.postMessage(JSON.parse(salida)); //Devuelve la salida en formato JSON
}

Se puede ver que se toman los parámetros de volatilidad, media y base desde el JSON de entrada desde el hilo principal. A partir de ahí, llamando a la función siguiente_paso, se va generando mediante un bucle el listado de puntos del camino aleatorio. Se hace en una cadena de texto con formato JSON para posteriormente ser transformada a objeto JSON con la función JSON.parse. Finalmente se envía al hilo principal a través de self.posMessage.

La operación siguiente_paso genera el siguiente valor del camino aleatorio, partiendo del valor actual y aplicándole un número aleatorio de una función normal 0,1 -N(0,1)- . Para su cálculo se opera con la volatilidad y el crecimiento medio indicado. Alta volatilidad permite obtener caminos más dispersos. La media es el crecimiento medio orgánico en porcentaje de cada paso.

function siguiente_paso(media, volatilidad, anterior)
{
 return (anterior+((anterior*media)+(volatilidad*anterior*randN())));
}

La función randN() es la que mayor calculo requiere y hace costoso el programa. Se emplean 100.000 iteraciones para su cálculo, aunque probablemente el óptimo sea mucho menor (12). Se ha elegido un número tan alto de iteraciones para hacer artificialmente costoso (pero preciso) el programa e ilustrar claramente la utilidad de los web workers:

function randN()
{
 var iteraciones = 100000; //lo óptimo son sólo 12, pero para simular carga de CPU
 var salida = 0;
 for(i=0;i<iteraciones;i++)
 {
 salida+=Math.random();
 }
 salida-=(iteraciones/2);
 salida*=Math.sqrt(12/iteraciones);

return salida;
}

Finalmente se ha incluíduo un mecanismo de parada del hilo que actúa en el bucle principal mediante el valor booleano de la variable “stop”, que se comprueba en cada iteración del bucle si debe continuar con el proceso o parar. La API de Web Workers permite enviar otros tipos de mensajes más allá del método “postMessage()”. Mediante el método “terminate()” enviamos un mensaje al Web worker que activa el evento “close” y que se captura mediate el listener correspondiente. Por tanto debemos añadir el siguiente código a nuestro worker:

self.addEventListener('close',function(e)
{
 stop=true;
});

La mejor manera de para un hilo como el que hemos desarrollado es haciendo una parada controlada del proceso, es decir, generando una serie de números completa. Para ello nos valdremos de una vaiable global que se modifica mediante el evento close y que está dentro del bucle de ejecución del worker. De este modo, si esta variable es true se parará el hilo y se cerrará, saliendo del bucle y cerrando el hilo mediante el método “close()”

if (!stop)
 main(e);
else
{
 break;
 self.close();
}

En el lado del hilo principal, el mecanismo consiste en lanzar tantos workers como se indique y registrar un listener por cada uno de ellos con el fin de recibir los mensajes de los hilos. Se guarda la referencia en un array para poder terminarlos posteriormente. El array es una variable global al hilo.

function start_workers()
{
 var num_workers = parseInt(document.getElementById("hilos").value);
 for (i = 0 ;i<num_workers;i++)
 {
 array_workers[i] = new Worker('js/w1.js');
 array_workers[i].addEventListener('message',function(e){
 pinta_serie_canvas(e.data);
 });
 array_workers[i].postMessage({'volatilidad':document.getElementById("volatilidad").value,'media':document.getElementById("media").value,'base':100,'iteraciones':document.getElementById("iteraciones").value}); //ojo:
 }
 }
 

Como se puede ver en el código, se instancia cada objeto web worker pasándole por parámetro el fichero javascript que define su comportamiento y que se ha descrito en líneas anteriores. Posteriormente se hace la llamada mediante la función postMessage. Se le pasa por parámetro JSON los valores de la volatilidad, media, iteraciones y valor 100 de partida. Como se puede ver se leen de unos input text que tienen id’s correspondientes. Cada web worker se mete en un array para tener su referencia.

El listener para cada mensaje de vuelta de los hilos, que contiene el paseo aleatorio de 100 números en formato JSON, hace una llamada la función pinta_serie_canvas, que lo dibuja en un canvas de HTML5

function pinta_serie_canvas(serie)
{
 var x_step = Math.max(1,Math.floor(800/serie.length)); //incremento eje X, al menos 1. siempre entero

 var canvas = document.getElementById('canvas');
 var context = canvas.getContext('2d');
 context.strokeStyle = generaColor();
 context.beginPath();

 var nuevaPosX = 0;
 var nuevaPosY = 250;
 for(j=1;j<serie.length;j++) //el primer valor es la base, es el de partida
 {

 context.moveTo(nuevaPosX,nuevaPosY); //mueve el cursor a la posición de partida para este paso
 nuevaPosX +=x_step;
 nuevaPosY +=-Math.floor(Math.round(serie[j].V - serie[j-1].V)); //avanza la diferencia que haya en v.absoluto

 context.lineTo(nuevaPosX,nuevaPosY);
 context.stroke(); //pinta la línea
 }

 context.closePath();
}

Básicamente se trata de partir del valor medio de la parte izquierda del canvas (punto 0, 250; ya que el canvas tiene 800*500 píxeles) e ir incrementando los valores de X (en 8 píxeles) y de Y (en tantos píxeles como números enteros haya de diferencia entre el punto actual y el siguiente) e ir pinando una línea mediante lineTo. También se genera un color diferente para cada camino pintado. La forma de manejar el canvas es sencilla.

Finalmente se añade el mecanismo para cancelar la ejecución de cada hilo. Tenemos guardadas su referencias dentro de un array, que recorreremos mandando el mensaje de close -mediante terminate()- a cada uno. Este proceso lo realiza la función terminate_workers();

function terminate_workers()
{
 for(l=0;l<array_workers.length;l++)
 {
 array_workers[l].terminate();
 array_workers[l]=null;
 }
}

Mediante la función terminate se envía un mensaje de tipo “close” al hilo, que como se ha visto antes, pone a true la variable “stop” que es tenida en cuenta en el bucle principal del worker.

Finalmente se ha incluído código HTML para crear la interfaz y algo de JavaScript para poder ligar los eventos de los botones al programa principal.

Podéis descargar el ejemplo haciendo un clone del repositorio en: https://github.com/4lberto/webworkers_paseo_aleatorio

Obsérvese cómo el proceso de cálculo de cada camino aleatorio no bloquea la interfaz web del usuario.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s