1. Code
  2. JavaScript

Primeros pasos con Web Workers

Scroll to top

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

Uno de los muchos objetivos de diseño del lenguaje JavaScript era mantenerlo de un solo hilo y, por extensión, simple. Aunque debo admitir que, dada la idiosincrasia de las construcciones del lenguaje, ¡es todo menos sencillo! Pero lo que queremos decir con "un solo hilo" es que solo hay un hilo de control en JavaScript; Sí, lamentablemente, su motor de JavaScript solo puede hacer una cosa a la vez.

Ahora, ¿no suena demasiado restrictivo para hacer uso de procesadores de múltiples núcleos que están inactivos en su máquina? HTML5 promete cambiar todo eso.


Modelo de un solo hilo de JavaScript

Los trabajadores web viven en un mundo restringido sin acceso a DOM, ya que DOM no es seguro para subprocesos.

Una escuela de pensamiento considera la naturaleza de un solo hilo de JavaScript como una simplificación, pero la otra lo descarta como una limitación. El último grupo tiene un buen punto, especialmente cuando las aplicaciones web modernas hacen un uso intensivo de JavaScript para manejar eventos de IU, consultar o encuestar las API del lado del servidor, procesar grandes cantidades de datos y manipular el DOM según la respuesta del servidor.

Ser capaz de hacer tanto en un solo hilo de control mientras se mantiene una interfaz de usuario sensible es a menudo una tarea desalentadora, y obliga a los desarrolladores a recurrir a hacks y soluciones alternativas (como usar setTimeout(), setInterval(), o usar XMLHttpRequest y Eventos DOM) para lograr la concurrencia. Sin embargo, vale la pena señalar que estas técnicas definitivamente proporcionan una forma de realizar llamadas asíncronas, pero que el no bloqueo no necesariamente significa concurrente. John Resig explica por qué no puedes ejecutar nada en paralelo en su blog.

Las limitaciones

Si ha trabajado con JavaScript durante un período de tiempo razonable, es muy probable que haya encontrado el siguiente cuadro de diálogo molesto que indica que algunas secuencias de comandos tardan demasiado en ejecutarse. Sí, casi cada vez que su página deja de responder, la razón puede atribuirse a algún código JavaScript.

Damn this slow machine !!Damn this slow machine !!Damn this slow machine !!

Estas son algunas de las razones por las que su navegador podría colgar sus botas mientras ejecuta su script:   

  • Manipulación excesiva de DOM: la manipulación de DOM es quizás la operación más costosa que puede hacer con JavaScript. En consecuencia, una gran cantidad de operaciones de manipulación de DOM hacen que su script sea un buen candidato para refactorización.
  • Bucles sin fin: nunca está de más escanear su código en busca de bucles anidados complejos. Estos tienden a hacer mucho más trabajo que lo que realmente se necesita. Quizás pueda encontrar una solución diferente que proporcione la misma funcionalidad.
  • Combinando los dos: lo peor que podemos hacer es actualizar repetidamente el DOM dentro de un bucle cuando existen soluciones más elegantes, como el uso de un DocumentFragment.

Web Workers al rescate...

...el no bloqueo no significa necesariamente concurrente...

Gracias a HTML5 y los trabajadores web, ahora puede generar un nuevo hilo: proporcionar una verdadera asincronía. El nuevo trabajador puede ejecutarse en segundo plano mientras el subproceso principal procesa los eventos de IU, incluso si el subproceso de trabajo está ocupado procesando una gran cantidad de datos. Por ejemplo, un trabajador podría procesar una gran estructura JSON para extraer información valiosa para mostrar en la interfaz de usuario. Pero basta de mis blabbering; Veamos algún código en acción.

Creando un trabajador

Normalmente, el código que pertenece a un trabajador web reside en un archivo JavaScript separado. El hilo primario crea un nuevo trabajador especificando el URI del archivo de script en el constructor Worker, que carga y ejecuta de forma asíncrona el archivo JavaScript.

1
var primeWorker = new Worker('prime.js');

Iniciar un trabajador

Para iniciar un trabajador, el hilo principal publica un mensaje al trabajador, como este:

1
var current = $('#prime').attr('value');
2
primeWorker.postMessage(current);

La página principal puede comunicarse con los trabajadores utilizando la API postMessage, que también se utiliza para la mensajería de origen cruzado. Además de enviar tipos de datos primitivos al trabajador, la API postMessage también admite pasar estructuras JSON. Sin embargo, no puede pasar funciones porque pueden contener referencias al DOM subyacente.

Los hilos padre y trabajador tienen su propio espacio separado; Los mensajes pasados de un lado a otro se copian en lugar de compartirlos.

Detrás de escena, estos mensajes se serializan en el trabajador y luego se retiran de serie en el extremo receptor. Por esta razón, no se recomienda enviar grandes cantidades de datos al trabajador.

El hilo principal también puede registrar una devolución de llamada para escuchar cualquier mensaje que el trabajador envíe después de realizar su tarea. Esto permite que el subproceso primario realice la acción necesaria (como actualizar el DOM) después de que el trabajador haya desempeñado su parte. Echa un vistazo a este código:

1
primeWorker.addEventListener('message', function(event){
2
    console.log('Receiving from Worker: '+event.data);
3
    $('#prime').html( event.data );
4
});

El objeto event contiene dos propiedades importantes:    

  • target: se utiliza para identificar al trabajador que envió el mensaje; Principalmente útil en un entorno de múltiples trabajadores.
  • data: el mensaje publicado por el trabajador de nuevo a su hilo principal.

El propio trabajador está contenido en prime.js y se registra para el evento message, que recibe de su padre. También utiliza la misma API postMessage para comunicarse con el hilo principal.

1
self.addEventListener('message',  function(event){
2
    var currPrime = event.data, nextPrime;
3
    setInterval( function(){
4
5
    nextPrime = getNextPrime(currPrime);
6
    postMessage(nextPrime);  
7
    currPrime = nextPrime;
8
9
    }, 500);
10
});

Los trabajadores web viven en un entorno restringido y seguro para subprocesos.

En este ejemplo, simplemente encontramos el siguiente número primo más alto y repetidamente publicamos los resultados en el hilo principal, que a su vez actualiza la interfaz de usuario con el nuevo valor. En el contexto de un trabajador, tanto como self y this se refieren al ámbito global. El trabajador puede agregar un detector de eventos para el evento message, o puede definir el controlador onmessage para escuchar cualquier mensaje enviado por el hilo principal.

La tarea de encontrar el siguiente número primo obviamente no es el caso de uso ideal para un trabajador, pero se ha elegido aquí para demostrar el concepto de pasar mensajes. Más adelante, exploramos casos de uso prácticos y posibles en los que el uso de un trabajador web realmente cosecharía beneficios.

Trabajadores que terminan

Los trabajadores son intensivos en recursos; son hilos a nivel del sistema operativo. Por lo tanto, no desea crear una gran cantidad de subprocesos de trabajo, y debe terminar el trabajador web después de que complete su trabajo. Los trabajadores pueden terminar ellos mismos, así:

1
self.close();

O un hilo padre puede terminar un trabajador:

1
primeWorker.terminate();

Seguridad y restricciones

Dentro de un script de trabajo, no tenemos acceso a los muchos objetos JavaScript importantes como document, window, console, parent y, lo que es más importante, no hay acceso al DOM. No tener acceso a DOM y no poder actualizar la página parece demasiado restrictivo, pero es una decisión de diseño de seguridad importante. Solo imagine el caos que podría causar si varios subprocesos intentan actualizar el mismo elemento. Por lo tanto, los trabajadores web viven en un entorno restringido y seguro para subprocesos.

Dicho esto, puede seguir utilizando trabajadores para procesar datos y devolver el resultado al hilo principal, que luego puede actualizar el DOM. Aunque se les niega el acceso a algunos objetos JavaScript bastante importantes, los trabajadores pueden usar algunas funciones como setTimeout()/clearTimeout(), setInterval()/clearInterval(), navigator, etc. También puede usar los objetos XMLHttpRequest y localStorage dentro el trabajador.

Restricciones Same Origin

En el contexto de un trabajador, tanto como self y this se refieren al ámbito global.

Para comunicarse con un servidor, los trabajadores deben seguir la política same-origin. Por ejemplo, un script alojado en http://www.example.com/ no puede acceder a un script en https://www.example.com/. Aunque los nombres de host son los mismos, la misma política original establece que el protocolo también debe ser el mismo. Normalmente, esto no es un problema. Es muy probable que escriba al trabajador, al cliente y que los sirva desde el mismo dominio, pero saber la restricción siempre es útil.

Problemas de acceso local con Google Chrome

Google Chrome impone restricciones para acceder a los trabajadores localmente, por lo que no podrá ejecutar estos ejemplos en una configuración local. Si desea usar Chrome, entonces debe hospedar estos archivos en algún servidor o usar el indicador --allow-file-access-from-files al iniciar Chrome desde la línea de comandos. Para OS X, inicie Chrome de la siguiente manera:

1
$ /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --allow-file-access-from-files

Sin embargo, el uso de este indicador no se recomienda en un entorno de producción. Por lo tanto, lo mejor que puede hacer es alojar estos archivos en un servidor web y probar a sus trabajadores web en cualquier navegador compatible.

Depuración de trabajadores y manejo de errores

No tener acceso a console hace que esto sea un tanto trivial, pero gracias a las Herramientas para desarrolladores de Chrome, uno puede depurar el código del trabajador como si fuera cualquier otro código JavaScript.

Damn this slow machine !!Damn this slow machine !!Damn this slow machine !!

Para manejar cualquier error generado por los trabajadores web, puede escuchar el evento error, que llena un objeto ErrorEvent. Puede inspeccionar este objeto para conocer la causa detallada del error.

1
primeWorker.addEventListener('error', function(error){
2
    console.log(' Error Caused by worker: '+error.filename
3
        + ' at line number: '+error.lineno
4
        + ' Detailed Message: '+error.message);
5
});

Múltiples hilos de trabajador

Si bien es común tener varios subprocesos de trabajo que dividen el trabajo entre ellos, es necesario tener cuidado. La especificación oficial especifica que estos trabajadores son relativamente pesados ​​y se espera que sean scripts de larga duración que se ejecuten en segundo plano. Los trabajadores web no están destinados a ser utilizados en grandes cantidades debido a su alto costo de rendimiento de inicio y un alto costo de memoria por instancia.

Breve introducción a los trabajadores compartidos

La especificación describe dos tipos de trabajadores: dedicados y compartidos. Hasta ahora, hemos visto ejemplos de trabajadores dedicados. Están directamente vinculados a su script / página creadora en el sentido de que tienen una relación de uno a uno con el script / página que los creó. Los trabajadores compartidos, por otro lado, pueden compartirse entre todas las páginas desde un origen (es decir, todas las páginas o scripts en el mismo origen pueden comunicarse con un trabajador compartido).

Para crear un trabajador compartido, simplemente pase la URL de la secuencia de comandos o el nombre del trabajador al constructor SharedWorker.

La principal diferencia en la forma en que se utilizan los trabajadores compartidos es que están asociados con un port para realizar un seguimiento del script principal que accede a ellos.

El siguiente fragmento de código crea un trabajador compartido, registra una devolución de llamada para escuchar cualquier mensaje publicado por el trabajador y publica un mensaje al trabajador compartido:

1
var sharedWorker = new SharedWorker('findPrime.js');
2
sharedWorker.port.onmessage = function(event){
3
    ...
4
}
5
6
sharedWorker.port.postMessage('data you want to send');

De manera similar, un trabajador puede escuchar el evento connect, que se recibe cuando un nuevo cliente intenta conectarse al trabajador y luego le envía un mensaje en consecuencia.

1
onconnect = function(event) {
2
    // event.source contains the reference to the client's port

3
    var clientPort = event.source;
4
    // listen for any messages send my this client

5
    clientPort.onmessage = function(event) {
6
        // event.data contains the message send by client

7
        var data = event.data;
8
        ....
9
        // Post Data after processing

10
        clientPort.postMessage('processed data');
11
    }
12
};

Debido a su naturaleza compartida, puede mantener el mismo estado en diferentes pestañas de la misma aplicación, ya que ambas páginas en diferentes pestañas usan la misma secuencia de comandos de trabajador compartido para mantener e informar el estado. Para obtener más detalles sobre los trabajadores compartidos, lo invito a leer la especificación.


Casos prácticos de uso

Los trabajadores web no están destinados a ser utilizados en grandes cantidades debido a su alto costo de rendimiento de inicio y un alto costo de memoria por instancia.

Un escenario de la vida real puede ser cuando se ve obligado a lidiar con una API de terceros sincrónica que obliga a la cadena principal a esperar un resultado antes de pasar a la siguiente declaración. En tal caso, puede delegar esta tarea a un trabajador recién creado para aprovechar la capacidad asíncrona en su beneficio.

Los trabajadores web también sobresalen en las situaciones de sondeo en las que continuamente se sondea un destino en segundo plano y se envía un mensaje al hilo principal cuando llegan datos nuevos.

Es posible que también deba procesar una gran cantidad de datos devueltos por el servidor. Tradicionalmente, el procesamiento de una gran cantidad de datos afecta negativamente la capacidad de respuesta de la aplicación, lo que hace que la experiencia del usuario sea inaceptable. Una solución más elegante dividiría el trabajo de procesamiento entre varios trabajadores para procesar partes no superpuestas de los datos.

Otros casos de uso podrían ser analizar fuentes de video o audio con la ayuda de múltiples trabajadores web, cada uno trabajando en una parte predefinida del problema.


Conclusión

Imagine la potencia asociada con varios subprocesos en un entorno de otro modo, de un solo subproceso.

Al igual que con muchas cosas en la especificación HTML5, la especificación del trabajador web continúa evolucionando. Si planea trabajar con trabajadores de la web, no estará de más echarle un vistazo a las especificaciones.

La compatibilidad con varios navegadores es bastante buena para trabajadores dedicados con versiones actuales de Chrome, Safari y Firefox. Incluso IE no se queda atrás con IE10 tomando la carga. Sin embargo, los trabajadores compartidos solo son compatibles con las versiones actuales de Chrome y Safari. Sorprendentemente, la última versión del navegador de Android disponible en Android 4.0 no es compatible con los trabajadores web, a pesar de que fueron compatibles con la versión 2.1. Apple también incluyó soporte para trabajadores web a partir de iOS 5.0.

Imagine la potencia asociada con varios subprocesos en un entorno de otro modo, de un solo subproceso. ¡Las posibilidades son infinitas!