Desde que los navegadores tenían soporte para Javascript, su código siempre se ha ejecutado en un único hilo. Los scripts que se desarrollan para el navegador están orientados a eventos y estos se van ejecutando en una especie de cola. Es decir, conforme se van generando los eventos estos van a una cola de tareas y Javascript los va procesando de uno en uno en un bucle.

Web Workers, hilos en tu navegador

HTML5 trae como novedad los Web Workers, que son procesos de javascript separados. Por lo tanto cada Worker tiene su propio bucle de procesamiento de tareas/eventos. El Worker se inicia desde el hilo principal del navegador y puede comunicarse con este y otros Workers mediante el envío de mensajes.

¿Son realmente necesarios? Bueno, hasta ahora con el asincronismo rampante como por ejemplo el de Ajax nos ha ido bien y ya llevamos unos años moviendo datos y soportando eventos. Peeero HTML5 viene con una serie de novedades que pueden agradecer ese tipo de mecanismos: por ejemplo el canvas (canvas es un lienzo), ese panel donde podemos pintar gráficos, la geolocalización, o las propias bases de datos indexedDB pueden beneficiarse de los hilos.

Limitaciones

Los Web Workers tienen su propio hilo a su disposición pero curiosamente tienen limitaciones muy considerables. ¿Te puedes creer que no puedes hacer un alert desde un worker? Tal cual, no tienes acceso al objeto predefinido window y a algunos otros solo puedes acceder en modo solo-lectura. Estas serían algunas de las limitaciones más importantes (a la velocidad que se transforma la web puede que todo esto vaya cambiando, revisa la fecha estelar de este post):

  • Solo pueden comunicarse mediante mensajes: no existen estados compartidos ni variables/estructuras compartidas.
  • Son pesados: no están pensados para levantar 100 workers, se habla de 10 como mucho.
  • No se pueden depurar ni testear: lo que ocurre en el Worker es una caja negra a día de hoy
  • Solo tipos simples en contenido de mensajes: esto puede variar pero no todos los navegadores soportan tipos complejos así que debes usar int, String,... pero ahora es cuando te ajustas tus gafas de pasta y piensas: mmmm JSON.stringify y serializo lo que quiera.
  • Sin acceso a console: al menos lo que he podido comprobar con mis medios
  • Sin acceso al DOM: olvídate de getElementById(), podrás usar un jquery limitado
  • Sin acceso a window: olvídate de los dialogs tipo alert, confirm,...
  • No hay localStorage ni sessionStorage: aunque sí tendrás acceso a IndexedDB

Según se mire, esas limitaciones pueden tener sus ventajas ya que en cierto modo orientan a los Workers a hacer un tipo de tareas no vínculadas al interfaz sino centradas en una tarea concreta. Además el hecho de que no haya estado compartido nos quita de un plumazo todas la necesidades de sincronización y los problemas de bloqueo commo el deadlock. También abre un inmenso campo abonado para que la gente proporcione sus propios mecanismos de sincronización.

Para poder contar con alguna ayuda, tenemos por ejemplo Jquery Hive que aparte de interfaces para crear Workers también nos facilita un subconjunto de jquery en el worker ($.get() , $.post() ,...)

Un ejemplo simple de Web Worker

Vamos a ver cómo crear un de Worker. Es tan simple como instanciar un objeto de la clase Worker pasándole como parámetro un fichero de código .js. Esto se haría en la página html (la que contiene el hilo principal); creamos la instancia, mandamos mensajes y establecemos un callback para procesar los mensajes del Worker.



            // This is how we create a Worker instance
			var worker = new Worker('helloWorker.js');

            // We send a message to worker
            console.log('Sending a message to Worker from main thread:');
			worker.postMessage('World' );

            // We set a callback for every message received from Worker
            worker.onmessage = function (event){
                // event.target: this is the worker object
                console.log("Sender object: " + event.target);
                // event.data: the message content
                console.log("Received message: " + event.data);
            };


Y este sería el contenido del Worker, del fichero helloWorker.js

/**
* helloWorker.js
* a simple worker sending/receiving messages
* @author Pello Xabier
*/

var workerName = 'Anhell';

self.postMessage('Hello');
self.postMessage('I am ' + workerName +' the Worker');

// We set this callback to receive messages from main thread
self.onmessage = function (event) {
	self.postMessage('Worker thread> Message received: ' + event.data);
};

Así lo vemos en la consola web de un firefox en linux:

Logs de ejemplo simple de web workers
No todo son limitaciones

Bueno, al menos algunas cosillas sí que se pueden hacer oficialmente desde un Worker. Se supone, y digo supone porque con los navegadores nunca se sabe, que en un Worker disponemos de:

  • .self: como se ve en el ejemplo anterior, es una referencia a sí mismo en el Worker
  • .close(): un interesante método para terminar el worker.
  • importScripts('js/miscript.js'): un método para cargar scripts, puede resultar muy útil pero ojo, si cargas jquery te fallará porque jquery hacce referencia a window.
  • Objetos predefinidos navigator y location: en modo solo lectura
  • XMLHttpRequest: para ajax a pelo, pero teniendo el jQuery Hive igual no compensa.
  • setTimeout y setInterval: lás míticas funciones para ejecutar cosas en un determinado tiempo
Las dos últimas son muy interesantes si vamos a trabajar con Workers. Para refrescar tu memoria, setInterval(func,miliseg) nos permite ejecutar una función una y otra vez con determinados intervalos de tiempo mientras que setTimeout(func,miliseg) ejecuta una función una vez pasado un determinado tiempo.

Un par de Workers

Vamos a ver un par de Workers compitiendo, Se trata de una Worker llamado Spaceship que va avanzando posiciones en un intervalo de tiempo. Para que no vaya a saco se usa la función setTimeout. En la página se van actualizando dos barras de progreso conforme los SpaceShips avanzan. Si todo va bien igual puedes probarlo en este propio post.

/**
* Web worker that simulates a spacheShip flying
* It sends a message to main thread to notify the distance travelled
* spacheShip.js
* @author Pello Xabier Altadill Izura
* @greetz Han Solo fans
*/

var distance = 0;
var total = 400;
var randomDistance = 0;
var speed = 10;
var ship = "spaceship1";

importScripts('js/randomLib.js');

/**
* sets space ship name
*/
function setSpaceship (name) {
	ship = name;
}

/**
* movesSpaceship in intervals
* untils distance is 0 or negative
*/
function moveSpaceship () {
		setTimeout("move()",100);
}

/**
* moves the spaceship and notifies the main thread
*/
function move () {
	randomDistance = -1;

	if (distance < total) {
		randomDistance = random(speed);
		// We send a message informing about ship name and distance moved and the remaining distance
		self.postMessage(ship + ":" + randomDistance+":"+distance);
		distance += randomDistance;
		moveSpaceship();
	}

}

// We set this callback to receive messages from main thread
// If we receive move now, the movement begins
self.onmessage = function (event) {
	self.postMessage('Spaceship thread> Message received: ' + event.data);
	if (event.data == "move")
		moveSpaceship();
};

Bien, el código es bastante mejorable y me gustaría poder darle más forma de POO pero bueno, es una primera aproximación. Si no te funciona pues al menos puedes ver el código fuente de este post.

WebWorkers sample
Spaceship1

Spaceship2