Ejemplo de Web Workers, los hilos javascript
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:
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
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.