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>
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.
- 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.
- 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
- 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
- Luego
se procesa durante 8ms el proceso handleMouseClock(),
15 al 23 ms.
- 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);
// 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();
<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>
</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 />
<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>
<div id="output"></div>
<script src="HolaWebWorkersJSON.js"
type="text/javascript"></script>
</body>
</html>
</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.";
}
// 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);
// 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);
_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
Publicar un comentario