Trabajando con WebWorkers



Tecnologias para el desarrollo de aplicaciones orientadas a la Internet.














Elias Cassal Baldiviezo.







Usando Web Workers

Los web Workers o tambien llamdos trabajadores para la web son recursos nuevos que nos ofrece javascript y html5 para poder desarrollar aplicaciones web que soporten el multiproceso.

Desde su inicio javascript nace copmo um leguaje eminentemente orientado a desarrollo de aplicaciones web y es de mucha ayuda al proporcionar interactividad dentro de los sítios web pero tenia una limitante muy grande pues no podia manejar la multitarea.

Nunca se penso que el desarrollo de javascript llegara a convertirlo en un lenguaje de programacion de proposito general propiamente dicho, que nos permite realizar desde las interfaces para la vista asi como tambien las implementaciones del lado del servidor con un ejemplo real y claro como es  NODEjs.

Una de las grandes incapacidades de JavaScript era no poder trabajar con multitareas, lo que obligaba a que todas las funciones que ejecutábamos en JavaScript tuvieran que concluir antes de que otra pudiera iniciar.  Es por esta razon que se decide implementar los WebWorkers. Esto lleva a concebir las aplicaciones web en otro nível puesto que ahora, con esta características se podia competir con las aplicaciones de escritório que se escribian solamente en lenguajes como Java o C.


Una mirada mas profunda aun a los webworkers, nos permitirá estudiar a profundida una de las APIs (interfaz de programación de aplicaciones) con las que cuenta javascript, con las que se pueden realizar aplicaciones mas completas y complejas que posibiliten manejar la concurrencia, utilidad que también nos va a servir para utilizar la funcionalidad de los procesadores modernos que hoy en su arquitectura tienen varios nucleos o CPUs logicas.

La Web antes de los Workers
Para explicar un poco en que consiste una pagina web esta maneja un thread principal o hilo principal de ejecución, que es el encargado de manejar todos los elementos del DOM elementos visuales y no visuales, y sus tareas relacionadas, nos referimos a los refrescos de pagina a las animaciones eventos de entrada, información de los usuarios, Etc…

Antes de los WebWorkers era habitual que las implementaciones de tareas complejas en una pagina web bloquearan este thread principal. Ese estado penoso derivado de la sobrecarga a este hilo principal se traduce en una degradación de la eficiencia de un sitio web, toda vez que la pagina quedara colgada y el usuario no podrá interactuar con su aplicación, con esta experiencia mala lo que procede es que usuario decida cerrar la pestaña y buscar una solución mas eficiente, lo que es un inconveniente si nosotros prestamos servicios a través de  la web en nuestro modelo de negocios.
Hasta no hace mucho estos inconvenientes no eran muy frecuentes. Y los desarrolladores tenían soluciones ingeniosas, como por ejemplo se trataba de simular la ejecución de tareas en paralelo, utilizando los métodos setTimeout() y setInterval(). Las peticiones HTTP se pueden también ejecutar de manera asíncrona con la ayuda del objeto XMLHttpRequest que evita el cuelgue de la UI mientras se cargan recursos desde servidores remotos. Finalmente, los Eventos del DOM nos permiten programar aplicaciones dando la apariencia de que ocurren varias cosas al mismo tiempo.

Para analizar mejor utilizaremos un ejemplo:

En el siguiente código ejecutaremos script de JavaScript y veremos en la imagen posterior que es lo que sucede en el navegador

<script type="text/javascript">
    function init(){
      { parte del código que se ejecuta en 5ms } 
      Se activa mouseClickEvent
      { parte del código que se ejecuta en 5ms }
      setInterval(timerTask,"10");
      { parte del código que se ejecuta en 5ms }
   }

function handleMouseClick(){
   parte del código que se ejecuta en 8ms 
}

function timerTask(){
   parte del código que se ejecuta en 2ms 
}
</script>


 En la imagen se presenta un modelo que muestra lo que sucede en el navegador en un espacio de tiempo. Mostrando claramente la naturaleza no paralela de la ejecución de los procesos, mas bien el navegador esta formando una cola de procesos con sus correspondientes peticiones de ejecución.

  1. Desde el milisegundo 0 al 5 la función init() arranca una tarea que tarda unos 5ms en completarse, después el usuario activa un evento de pulsación de raton, el cual no se puede manejar aun porque aun se sigue ejecutando la función init() que esta con la asignación del thread o hilo principal en ese momento. El evento click del usuario se guardara en un buffer y se dispondrá posteriormente.
  2. Del 5 a 10 ms se sigue procesando la función init(), luego se pide planificar la llamada al TimerTask() en 10ms, función que por la lógica en la administración de procesos debería ejecutarse en el milisegundo 20
  3. Del 10 al 15 ms se precisan 5 ms mas para terminar de ejecutar la función init() (intervalo correspondiente al bloque amarillo de la imagen), ya liberado el hilo principal, recién se puede empezar a procesar las peticiones guardadas
  4.  Luego se procesa durante 8ms el proceso handleMouseClock(), 15 al 23 ms.
  5. Del 23 al 25 ms, la función timerTask() que estaba prevista para ejecutarse en el milisegundo 20 de la línea de tiempo, se desplaza unos 3 ms. Los otros puntos planificados (30 ms., 40 ms. etc.) se respetan ya que no hay más código capturando recursos de CPU.

Nota: Este ejemplo y el diagrama (en SVG o PNG, depende del mecanismo de detección de funcionalidad) se ha inspirado en el artículo HTML5 Web Workers Multithreading in JavaScript

Muchos lenguajes soportan la ejecución en varios threads (hilos de ejecución), esto aparece bajo la especificación WHATWG Web Workers, una API para la ejecución de javascript en segundo plano y en procesos independientes, es decir, que se pueden ejecutar cualquier script de una manera no secuencial, como habitualmente lo hace JavaScript pudiendo repartir la ejecución en varios procesos.

Una peculiaridad de los workers es que estos deben estar en un archivo diferente aunque no necesariamente como veremos en los ejemplos. Entonces cada worker usualmente contendrá su propio archivo y su thread y su hilo de ejecución diferente, por motivos de seguridad,  asi no podrán acceder directamente a las variables globales ni a los elementos del DOM, miestras estos se estén utilizando, algo asi como el modo de subprocesamiento multiple que utiliza java en la implementación de threads.

Ahora como creamos un worker, el proceso básico es la instanciación del objeto de tipo worker. A continuación proporcionaremos el código JavaScript necesario:
           
var miHolaWorker = new Worker('holaworkers.js');

Luego iniciamos las acciones en worker y con esto un thread en Windows enviándole un primer mensaje.

miHolaWorker.postMessage();

la forma de comunicarse de los wortkers con la pagina principal sera a través de paso de mensajes. Estos mensajes se pueden crear a través de cadenas normales o través de mesajes tipo json y xml aunque ya no se utiliza frecuentemente. Vamos a revisar un ejemplo senciloo de intercambio de mensajes:

function messageHandler(event) {
   // Accede a los datos del mensaje enviado desde la página principal
   var messageSent = event.data;
   // Prepara el mensaje para devolver
   var messageReturned = "¡Hola " + messageSent + " desde un Worker de la Web!";
   // Publica el mensaje de vuelta en la página principal
   this.postMessage(messageReturned);
}

// Declara la function de callback que se ejecutará cuando la página principal nos haga una llamada
this.addEventListener('message', messageHandler, false);

Aquí se ha definido dentro del archivo holaworkers.js  un hilo de ejecución que se ejecutara por separado, que puede recibir mensajes de nuestra pagina principal, hacer con estos datos algún proceso y retornarlos a través de otro mensaje, a la pagina principal osea el hilo de ejecución primario. Ahora escribiremos el código de recepción dentro de nuestra pagina principal osea las sentencias de Javascript necesarias para recibir el mensaje de retorno.

<!DOCTYPE html>
<html>
<head>
    <title>Hola web Workers</title>
</head>
<body>
    <div id="salida"></div>

    <script type="text/javascript">
       // Instanciamos el Worker
       var miHolaWorker = new Worker('holaworkers.js');
       // Se prepara para manejar el mensaje que devuelve
       // el worker
       miHolaWorker.addEventListener("message", function (event) {
          document.getElementById("salida").textContent = event.data;
       }, false);

    // Inicializa el worker enviándole un primer mensaje
    miHolaWorker.postMessage("Elias Cassal");

    // Detiene el worker con el comando terminate()
    miHolaWorker.terminate();
   miHolaworker = “undefined”;
    </script>
</body>
</html>

El resultado sera  la siguiente cadena de caracteres “Hola Elias Cassal desde un Worker de la Web”.

Es importante que recordemos que nuestro worker seguirá activo hasta que lo detengamos, acción muy importante puesto que una instancia de subprocesamiento consume memoria, también tiempo que se consume en el arranque inicial.

Existen  dos alternativas para terminar un worker

  • Desde la página principal llamando al comando terminate() Ejem. worker.terminate();
  • Desde el propio worker utilizando el comando close(). Ejem. worker.close();

Publicar mensajes utilizando JSON.-

Generalmente la mayoría de ocasiones los workers se intercambiarán datos más estructurados (los Web Workers pueden comunicarse unos con otros utilizando los canales de mensaje.)

Pero la única manera de enviar mensajes estructurados a un worker es mediante el uso de formato JSON. Es importante notar que ahora los navegadores que soportan actualmente los Web Workers son suficientemente avanzados como para soportar JSON de forma nativa.

Volvamos a nuestro ejemplo anterior. Vamos a añadir un objeto del tipo WorkerMessage. Este tipo se utilizará para enviar algunos comandos con parámetros a nuestros “Workers.

Vamos a utilizar el código simplificado de la página web HolaWebWorkersJSON.html:

<!DOCTYPE html>
<html>
<head>
    <title>Hola Web Workers JSON</title>
</head>
<body>
         <input id=inputForWorker />
         <button id=btnSubmit>Enviar al worker</button>
        <button id=killWorker>Detener el worker</button>          
        <div id="output"></div>

   
     <script src="HolaWebWorkersJSON.js" type="text/javascript"></script>
     </body>
</html>

Utilizamos la estrategia no obstructiva de Javascript que nos ayuda a disociar la parte visual de la lógica asociada. La lógica asociada reside en el archivo.
HolaWebWorkersJSON.js cuyo código es:

// HolaWebWorkersJSON.js asociado a HolaWebWorkersJSON.htm
// Nuestro objeto WorkerMessage se serializará y
// de-serializará automáticamente en el analizador JSON nativo
function WorkerMessage(cmd, parameter) {
    this.cmd = cmd;
   this.parameter = parameter;
}

// DIV de salida donde se mostrarán los mensajes devueltos por el worker
var _output = document.getElementById("output");

/* Comprueba si el navegador soporta Web Workers */
if (window.Worker) {
    // Obtiene la referencia de los otros 3 elementos HTML
    var _btnSubmit = document.getElementById("btnSubmit");
    var _inputForWorker = document.getElementById("inputForWorker");
    var _killWorker = document.getElementById("killWorker");

   // Instancia el Worker
    var miHolaWorker = new Worker('holaworkersJSON.js');
    // Se prepara para manejar el mensaje devuelto
    // por el worker
   miHolaWorker.addEventListener("message", function (event) {
       _output.textContent = event.data;
   }, false);

    // Arranca el worker con el comando 'init'
    miHolaWorker.postMessage(new WorkerMessage('init', null));

    // Añade el evento OnClick al botón Submit
    // que enviará algunos mensajes al worker
    _btnSubmit.addEventListener("click", function (event) {
       // Ya estamos enviando mensajes por medio del comando 'hello'
       miHolaWorker.postMessage(new WorkerMessage('hello', _inputForWorker.value));
    }, false);

    // Añade el evento OnClick al botón Kill
    // que debe parar el worker. Ya no se podrá utilizar más después de eso.
    _killWorker.addEventListener("click", function (event) {
       // Para el worker mediante el comando terminate()
       miHolaWorker.terminate();
       _output.textContent = "El worker se ha parado.";
    }, false);
}
else {
    _output.innerHTML = "El navegador no soporta Web Workers.";
}

Al final, el código para el Web Worker que debe tener el archivo holaworkerJSON.js es este:

function messageHandler(event) {
    // Accede al mensaje enviado por la página principal
    var messageSent = event.data;

    // Comprueba el comando enviado por la página principal
    switch (messageSent.cmd) {
       case 'init':
          // Puedes inicializar aquí algunos de tus modelos/objetos
          // que luego puedes utilizar en el worker (pero ten en cuenta su ámbito de uso!)
       break;
       case 'hello':
          // Prepara el mensaje que va a devolver
          var messageReturned = "Hola " + messageSent.parameter + " desde un thread distinto!";
          // Devuelve el mensaje a la página principal
          this.postMessage(messageReturned);
          break;
       }
}

// Define la función de callback que se activará cuando la página principal nos llame
this.addEventListener('message', messageHandler, false);


Como se puede apreciar el ejemplo es sencillo, sin embargo nos ayuda a entender la lógica de la programación subyacente. Por ejemplo, podemos utilizar esta misma estrategia de implementación para enviar algunos elementos de un juego que puedan manejarse desde un motor de inteligencia artificial o de física.

Elementos que no se puden acceder desde un worker.

Con esta imagen vamos a ver claramente a que elementos no podemos acceder desde un worker, por lo que tendríamos que regresar al hilo principal de ejecución a travesde los mensajes y ejecutar allí la ccion. En resumen no podemos acceder al DOM.


Como podemos ver no se tiene acceso al objeto Windows desde un Worker, por ende no podemos acceder al local storage. (No es compatible con el modelo de acceso concurrente thread-safe).

Depuración y manejo de errores
El manejo de errores de los Web Workers es muy sencillo, basta con suscribirse al evento OnError igual que hemos hecho antes con el evento OnMessage:

myWorker.addEventListener("error", function (event) {
    _output.textContent = event.data;
}, false);

Esto es lo único que nos ofrece la API de los Web Workers a nivel diseño para depurar el código.
Depuración de los Web Workers.-

Los navegadores han ido evolucionando y tienen dentro de su interfaz de inpeccion de elementos ya incluidas estas opciones de depuración de subprocesos como el casl de Google Chrome internet Explorer y Mozilla Firefox.

El WebWorker se puede depurar al igual que una página web normal. Chrome proporciona herramientas de debugging para WebWorkers en chrome: // inspect / # workers .

Ubique al webworker en ejecución deseado y haga click "inspeccionar". Se abrirá una ventana con la herramienta de desarrollo por separado dedicada a ese webworker.

En febrero de 2016, WebStorm lanzó soporte para la debugging de web workers .

El depurador JavaScript de WebStorm ahora puede llegar a puntos de interrupción dentro de estos Workers de segundo plano. Puede ir a través de los frameworks y explorar las variables de la misma manera que está acostumbrado. En la lista desplegable de la izquierda puede saltar entre los hilos de los trabajadores y el hilo principal de la aplicación.

Conclusión

No hay ningún concepto nuevo con respecto a los Web Workers, en cuanto a la forma en que deberíamos revisar y rediseñar nuestro código Javascript para hacerlo apto o no, para la ejecución en paralelo. Necesitaremos aislar la parte del código que consume más CPU y esta parte debe ser relativamente independiente del resto de la lógica de la página para evitar tener que esperar a la sincronización de tareas. Y lo más importante: el código debe ser independiente del DOM. Si ambas condiciones se cumplen, es recomendable utilizar Web Workers, ya que con ellos vamos a mejorar el rendimiento global de la aplicación














Comentarios

Entradas populares de este blog

Investigación #1