Nella programmazione JavaScript si fa spesso ricorso ad un modello di programmazione concorrente in base al quale possiamo pensare che diverse attività possano avvenire virtualmente in parallelo e possano comunicare tra di loro in maniera asincrona.
Pensiamo, ad esempio, al verificarsi di eventi: questi si possono verificare in maniera indipendente dall'esecuzione del flusso principale del nostro script e, virtualmente, possono verificarsi più eventi contemporaneamente. Se abbiamo previsto gestori di eventi per più eventi, ci aspettiamo che questi vengano eseguiti immediatamente al verificarsi del relativo evento.
Modelli di concorrenza, l'event loop
In realtà le cose non stanno proprio così. Il modello di concorrenza di JavaScript è diverso da quello di altri linguaggi come ad esempio C o Java.
Mentre infatti nei linguaggi di programmazione che supportano la concorrenza una porzione di codice di un thread può essere interrotta per mandare avanti l'esecuzione di un altro thread, in JavaScript tutto avviene in un unico thread.
Il modello di concorrenza in base al quale abbiamo l'illusione che più thread siano in esecuzione è quello dell'event loop: ogni evento inserisce un messaggio in una coda che viene elaborata sequenzialmente dal runtime di JavaScript in un ciclo infinito.
In pratica, un engine JavaScript non fa altro che verificare la presenza di messaggi nella coda ed eseguire il codice dell'eventuale gestore per passare poi al messaggio successivo. È importante aver chiaro che il codice eseguito tra un messaggio ed il successivo viene eseguito senza interruzioni. Qualsiasi evento che si verifica durante l'esecuzione di un ciclo dell'event loop non può interromperlo.
Comprendere il modello di concorrenza su cui si basa JavaScript è importante per capire il motivo di certi comportamenti e per poter scrivere codice efficiente.
Infatti, se questo meccanismo ha dalla sua parte un'estrema semplicità ed efficienza dovute all'assenza del cambio di contesto tra thread diversi, non è tuttavia immune da piccoli inconvenienti.
Timer... imprecisi
Consideriamo ad esempio il metodo setTimeout() dell'oggetto window. Esso esegue una funzione dopo un determinato numero di millisecondi:
var myTimer = setTimeout(function() {console.log("test")}, 5000);
Da questo snippet di codice ci aspettiamo che faccia apparire la scritta "test" sulla console dopo 5 secondi dall'inizio dell'esecuzione. In realtà questo non è garantito.
In base al meccanismo di concorrenza di JavaScript, allo scadere dei cinque secondi il timer genera un messaggio e lo inserisce nella coda degli eventi. Se il runtime non sta eseguendo del codice eseguirà immediatamente la funzione pianificata da setTimeout()
, altrimenti la funzione verrà eseguita non appena il runtime avrà terminato l'esecuzione corrente ed avrà smaltito la coda degli eventi. Questo potrebbe comportare un ritardo non prevedibile, dal momento che tutto dipende dall'impegno richiesto dal codice e dal numero di eventi che sono in attesa di essere gestiti.
In altre parole, quando specifichiamo il numero di millisecondi per il metodo setTimeout() dobbiamo considerarlo come il tempo minimo dopo il quale eseguire la funzione.
L'imprecisione dei timer legati al modello di concorrenza di JavaScript può generare addirittura situazioni inconsistenti o comunque diversi da come ce li attenderemmo. Consideriamo infatti il seguente codice:
function prima() {
console.log("prima");
setTimeout(seconda, 0);
console.log("terza");
quarta();
}
function seconda() { console.log("seconda"); }
function quarta() { console.log("quarta"); }
prima();
Poiché abbiamo specificato zero come intervallo di tempo per setTimeout()
, ci aspetteremmo che l'esecuzione della funzione seconda()
sia immediata. In realtà eseguendo il codice otterremo la seguente sequenza di stringhe:
prima terza quarta seconda
La funzione seconda()
verrà eseguita al termine dell'intera esecuzione della funzione prima()
e delle funzioni chiamate all'interno di essa. Ciò deriva dal fatto che il verificarsi dell'evento di scadenza del timer non fa altro che inserire un messaggio nella coda degli eventi, ma il runtime JavaScript è già occupato ad eseguire la funzione prima()
, per cui eseguirà la funzione seconda()
soltanto al termine delle sue attività.