Per mantenere responsiva l'interfaccia utente mentre si effettuano elaborazioni impegnative con JavaScript siamo costretti a ricorrere a qualche trucco sfruttando meglio che possiamo il ciclo di elaborazione del runtime.
L'uso di setTimeout()
è un esempio tipico di questo approccio. Essa ci consente di rendere in qualche modo asincrona l'elaborazione JavaScript, ma non parallela. In altre parole, qualsiasi elaborazione eseguita dall'interprete JavaScript non può avvenire in parallelo a un'altra: tutte le attività sono sequenziali, anche se possiamo spezzarle, inframezzarle con altre ed avere l'illusione di una forma di parallelismo.
Per sopperire a questa limitazione, la specifica HTML5 introduce un'importante novità in questo ambito: i Web Worker. Un Web Worker è un thread eseguito parallelamente all'esecuzione del thread principale del JavaScript engine.
Per comprendere come utilizzare questi componenti, proviamo a trasformare il codice visto negli esempi precedenti in modo da eseguirlo in un Web Worker.
Inseriamo innanzitutto il codice della nostra elaborazione intensiva in un file separato worker.js
:
function fase1() {
for(var i = 0;i<999999999;i++){}
}
function fase2() {
for(var i = 0;i<999999999;i++){}
}
Vediamo come creare un'istanza di Web Worker tramite il costruttore Worker():
var myWorker = new Worker("worker.js");
Scambi di messaggi tra thread
Per consentire un'interazione tra il thread principale e il thread secondario è previsto un meccanismo basato su scambi di messaggi. Ad esempio, per avviare l'elaborazione del nostro worker possiamo scrivere il seguente codice:
var myWorker = new Worker("worker.js");
myWorker.postMessage("start");
Il metodo postMessage() consente lo scambio di messaggi tra thread generando un evento. Il messaggio può essere costituito da dati di tipo primitivo o da oggetti. Tuttavia, dal momento che i thread hanno spazi di memoria separati, i messaggi vengono passati per valore e pertanto vengono serializzati e copiati nei rispettivi spazi di memoria. Per questo motivo è buona norma evitare di passare strutture complesse o grandi quantità di dati.
Abbiamo detto che l'invio di un messaggio genera un evento. Questo vuol dire che per ricevere il messaggio inviato dal thread
principale, il worker
dovrà intercettare e gestire questo evento. Il seguente codice mostra come fare:
self.addEventListener("message", function(event) {
if (event.data == "start") {
fase1();
fase2();
}
});
Come possiamo vedere, abbiamo gestito l'evento message come un qualsiasi evento JavaScript. In alternativa avremmo potuto associare il gestore dell'evento alla proprietà onmessage.
Nell'esempio abbiamo utilizzato self per fare riferimento al contesto globale del worker. In questo contesto, che è diverso dal contesto globale del thread principale, le parole chiave this
e self
rappresentano lo stesso oggetto.
L'oggetto event passato al gestore dell'evento prevede la proprietà data
che rappresenta il messaggio inviato dal thread
chiamante.
Lo stesso meccanismo utilizzato dal thread principale per comunicare con il worker può essere utilizzato per la comunicazione inversa, dal worker al thread principale. Ad esempio, per segnalare al thread principale l'avanzamento delle fasi di elaborazione possiamo prevedere l'invio di messaggi come mostrato nell'esempio:
function fase1() {
for(var i = 0;i<999999999;i++){}
}
function fase2() {
for(var i = 0;i<999999999;i++){}
}
self.addEventListener("message", function(event) {
if (event.data == "start") {
self.postMessage("Elaborazione in corso: fase 1");
fase1();
self.postMessage("Elaborazione in corso: fase 2");
fase2();
self.postMessage("Fine elaborazione");
}
});
Il thread
principale intercetterà a sua volta l'evento message
e lo gestirà di conseguenza. Ad esempio, nel nostro caso può aggiornare l'interfaccia utente visualizzando il messaggio sullo stato di avanzamento dell'elaborazione:
var msgDisplay = document.getElementById("msgDisplay");
var myWorker = new Worker("worker.js");
myWorker.postMessage("start");
myWorker.addEventListener("message", function(event) {
msgDisplay.innerHTML = event.data;
});
Con questo approccio manterremo un'interfaccia Web responsiva pur effettuando un'elaborazione impegnativa per JavaScript.
È importante tenere in considerazione il fatto che il thread associato al worker non termina con l'elaborazione associata. Esso rimane in attesa di eventuali nuovi messaggi consumando risorse di calcolo preziose come memoria e processore. È opportuno quindi terminare il worker non appena ha terminato il suo compito.
La terminazione del worker può avvenire dall'interno del worker stesso tramite il metodo close():
self.close();
o dal thread principale tramite il metodo terminate()
:
myWorker.terminate();
Gestione degli errori
Per gestire gli errori che possono verificarsi durante l'esecuzione di un worker
, possiamo fare ricorso all'evento error
, come mostrato nel seguente esempio:
myWorker.addEventListener("error", function(error) {
console.log("Si è verificato l'errore '" + error.message +
"' durante l'esecuzione del worker " + error.filename +
" in corrispondenza della linea " + error.lineno);
});
Come possiamo dedurre dall'esempio, l'oggetto che rappresenta l'errore verificatosi durante l'esecuzione del worker mette a disposizione tre importanti proprietà: il messaggio d'errore (message
), il file contenente il codice del worker (filename
) e il numero di linea in cui si è verificato l'errore (lineno
).
Vincoli dei Web Worker
I Web Worker rappresentano un'importante innovazione per il supporto della concorrenza in JavaScript. Tuttavia occorre tener conto della presenza di alcune importanti limitazioni.
Innanzitutto dall'interno di un worker non è possibile accedere al DOM, nè ad oggetti del browser come window
e console
. Anche se queste limitazioni potrebbero sembrare a prima vista un po' troppo restrittive, in realtà esse rispondono ad un'esigenza di sicurezza architetturale e, con alcuni accorgimenti, l'apparente limitazione è facilmente superabile.
Infatti, questa restrizione è dettata dal voler evitare un accesso incontrollato agli elementi del DOM e del browser che potrebbero creare situazioni di deadlock
, ma è sufficiente delegare al thread
principale il compito di riportare sul DOM il risultato delle elaborazioni di un worker
, come abbiamo mostrato nell'esempio.
Inoltre, è utile ricordare che ai worker si applica la same-origin policy applicata al thread principale, così un worker può interagire soltanto con lo stesso server da cui è stato scaricato lo script del thread principale.
Altro aspetto a cui prestare attenzione è il fatto che gli URI relativi utilizzati all'interno di un worker vengono risolti rispetto all'indirizzo dello script padre e non rispetto all'indirizzo della pagina.
Come ulteriore limitazione possiamo identificare lo scambio di messaggi e dati tra i thread. Abbiamo visto come sia da preferire il passaggio di dati primitivi o strutture semplici, evitando di passare strutture dati di grandi dimensioni per l'impatto che possono avere sulle risorse di sistema. È invece del tutto inibito il passaggio di funzioni.
Link Utili