Dedicated Workers vs Shared Workers

En el post anterior estuve hablando sobre la API para la creación de Web Workers en su forma más básica. Hoy me gustaría hablar de los dos tipos de Workers que existen dentro de la especificación a día de hoy: Dedicated Workers y Shared Workers. En el primer post se hace hincapié en el worker de tipo dedicado, ya que fue el primero que comenzaron a implementar los navegadores y es el más simple de los dos. En este post me gustaría comentar cuál es el funcionamiento para los Shared Workers, ya que la forma de gestionarlo difiere del anterior.

Múltiples conexiones

La gran diferencia entre ellos es que los de tipo compartido (Shared) permiten múltiples conexiones a una instancia a través de lo que se conoce como puertos (MessagePort interface). Además del creador de la instancia, otras partes de la aplicación pueden acceder al mismo worker y a la información contenida en él. En este primer ejemplo, haciendo uso de ASP.NET MVC, he creado una vista donde se carga tanto un Dedicated Worker como un Shared Worker, además de un enlace que solicita una segunda página a través de AJAX donde se realiza la misma operación:

@{
    ViewBag.Title = "title";
}
<h2>Main Page</h2>
@Ajax.ActionLink("Get partial view", "SecondPage", new AjaxOptions
{
    UpdateTargetId = "myPartial"
})
<div>
    <span id="messageMainPageSharedWorker"></span>
    <span id="messageMainPageDedicatedWorker"></span>
    <div id="myPartial"></div>
</div>
<script>
    window.onload = function () {
        var sharedWorker = new SharedWorker('/Scripts/sharedWorker.js');
        sharedWorker.port.onmessage = function (e) {
            messageMainPageSharedWorker.innerHTML += "<br />" + e.data;
        };
        sharedWorker.port.onerror = function (e) {
            messageMainPageSharedWorker.innerHTML += "<br />" + e.message;
        };
        sharedWorker.port.postMessage('ping! from Main page');
    };
</script>
<script>
    var worker = new Worker('/Scripts/webWorker.js');
    worker.onmessage = function (e) {
        messageMainPageDedicatedWorker.innerHTML += "<br />" + e.data;
    };
    worker.onerror = function (e) {
        messageMainPageDedicatedWorker.innerHTML += "<br />" + e.message;
    };
    worker.postMessage('ping! from Main page');
</script>

Con este ejemplo podemos ver las diferencias al crear una instancia de tipo Dedicated y de tipo Shared: Para el tipo compartido utilizamos el objeto SharedWorker en lugar de Worker. A través de él todas las acciones, manejadores de eventos, etcétera se harán a través de port (interfaz MessagePort) el cual se configura cuando se crea una conexión con el hilo secundario.
Por otro lado, se ha creado una partial view (la cual se cargará a petición en el div con id myPartial) donde tenemos el siguiente código:

@{
    ViewBag.Title = "Second Page";
}
<h2>Second Page</h2>
<span id="messageSecondPageSharedWorker"></span>
<span id="messageSecondPageWorker"></span>
<script>
    var sharedWorker = new SharedWorker('/Scripts/sharedWorker.js');
    sharedWorker.port.onmessage = function (e) {
        messageSecondPageSharedWorker.innerHTML += "<br />" + e.data;
    };
    sharedWorker.port.onerror = function (e) {
        messageSecondPageSharedWorker.innerHTML += "<br />" +  e.message;
    };
    sharedWorker.port.postMessage('ping! from Second page');
</script>
<script>
    var worker = new Worker('/Scripts/webWorker.js');
    worker.onmessage = function (e) {
        messageSecondPageWorker.innerHTML += "<br />" + e.data;
    };
    worker.onerror = function (e) {
        messageSecondPageWorker.innerHTML += "<br />" + e.message;
    };
    worker.postMessage('ping! from Second page');
</script>

Podríamos decir que el código es exactamente igual que el anterior, pero este se carga de manera asíncrona cuando hacemos clic en el enlace ubicado en la página principal.

Por último, sharedWoker.js será el archivo que se lanzará en segundo plano y configurará un puerto por cada conexión que se le solicite:

var connections = 0;
self.addEventListener("connect", function (e) {
    var port = e.ports[0];
    connections++;
    port.postMessage("<span style='color: green';>Connection # " + connections + "</span>");
    port.addEventListener("message", function (e) {
        port.postMessage("<strong>[Shared Worker]: </strong> " + e.data);
    });
    port.start();
});

En este archivo lo primero que tenemos es una variable llamada connections a la cual se le va sumando el número de conexiones que se van creando en el manejador del evento connect. Dentro del mismo , recuperamos el puerto que debemos configurar, enviamos un mensaje al usuario indicando que la conexión se ha realizado y capturamos los eventos message para ese «cliente». Por último debemos iniciar el puerto a través de start();

El resultado de este código es el siguiente:

Shared Workers vs Dedicated Workers

La primera página pinta su contenido y lanza una llamada tanto al Shared Worker como al Dedicated Worker. El primero de ellos muestra que tiene la conexión número 1 y acto seguido muestra el mensaje recibido desde el hilo secundario.
Después se lanza la llamada al worker dedicado donde muestra el mismo mensaje y el número de conexiones actuales. El contenido de este script ese el siguiente:

var connections = 0;
self.addEventListener("message", function (e) {
    connections++;
    postMessage("<strong>[Dedicated Worker]</strong> " + e.data + " <span style='color: red;'>Connection #" + connections + "</span>");
});

Si nos fijamos en el segundo apartado de la imagen, al haber hecho clic en el enlace Get partial view se recupera la Second Page, donde podemos ver que se ha realizado una nueva conexión al worker que ya teníamos (Connection # 2) y, sin embargo, en el caso del dedicated worker se lanza una nueva instancia y muestra como valor Connection # 1.

¿Puedo lanzar dos conexiones desde el mismo script para un Shared Worker?

Si, es posible y tan sencillo como crear un nuevo objeto apuntando al mismo archivo:

//Main page
var sharedWorker = new SharedWorker('/Scripts/sharedWorker.js');
var sharedWorker2 = new SharedWorker('/Scripts/sharedWorker.js');

¿Puedo tener más de una instancia de un mismo Shared Worker (mismo arhivo JavaScript)?

Existe un parámetro opcional para los objetos del tipo SharedWorker que nos permite asignar un alias a la instancia a la que queremos conectar. Si no hacemos uso de él todas las conexiones se realizarán a la instancia que apunte a ese archivo y no tenga asignado ningún nombre.

//Main page
var sharedWorker = new SharedWorker('/Scripts/sharedWorker.js');
var sharedWorker2 = new SharedWorker('/Scripts/sharedWorker.js', 'MySharedWorker');
//Second page
var sharedWorker = new SharedWorker('/Scripts/sharedWorker.js', 'MySharedWorker');
var sharedWorker3 = new SharedWorker('/Scripts/sharedWorker.js', 'MySharedWorker2');

He modificado ligeramente el archivo sharedWorker para saber a qué instancia está haciendo referencia cada mensaje:

var connections = 0;
self.addEventListener("connect", function (e) {
    var port = e.ports[0];
    connections++;
    port.postMessage("<span style='color: green';>Connection # " + connections + "</span>");
    port.addEventListener("message", function (e) {
        var sharedWorkerName = self.name ? " (" + self.name + " instance)": "";
        port.postMessage("<strong>[Shared Worker" + sharedWorkerName + "]: </strong> " + e.data);
    });
    port.start();
});

Modificando las conexiones tal y como muestro en el código anterior, el resultado obtenido es el siguiente:

Different Shared Workers alias

Espero que haya sido de utilidad.

Demo
Can I use Shared Web Workers?

¡Saludos!