JavaScript è un linguaggio a thread singolo e, allo stesso tempo, non bloccante, asincrono e concorrente. Questo articolo vi spiegherà come avviene.
Tempo di esecuzione
JavaScript è un linguaggio interpretato, non compilato. Ciò significa che ha bisogno di un interprete che converta i file JS codice a un codice macchina. Esistono diversi tipi di interpreti (noti come motori). I motori per browser più diffusi sono V8 (Chrome), Quantum (Firefox) e WebKit (Safari). Per inciso, V8 è utilizzato anche in un popolare runtime non-browser, Node.js.
Ogni motore contiene un heap di memoria, uno stack di chiamate, un ciclo di eventi, una coda di callback e una WebAPI con richieste HTTP, timer, eventi e così via, tutti implementati a modo proprio per un'interpretazione più rapida e sicura del codice JS.
Architettura di base del runtime JS. Autore: Alex Zlatkov
Filo singolo
Un linguaggio a thread singolo è un linguaggio con un singolo stack di chiamate e un singolo heap di memoria. Significa che esegue solo una cosa alla volta.
A pila è una regione continua di memoria, che alloca un contesto locale per ogni funzione eseguita.
A mucchio è una regione molto più grande, che memorizza tutto ciò che è allocato dinamicamente.
A stack di chiamata è una struttura di dati che registra sostanzialmente la posizione del programma.
Pila di chiamata
Scriviamo un semplice codice e seguiamo ciò che accade nello stack delle chiamate.
Come si può vedere, le funzioni vengono aggiunte allo stack, eseguite e successivamente eliminate. È il cosiddetto metodo LIFO (Last In, First Out). Ogni voce dello stack di chiamate è chiamata cornice della pila.
La conoscenza dello stack delle chiamate è utile per leggere le tracce dello stack degli errori. In genere, il motivo esatto dell'errore si trova in cima alla prima riga, anche se l'ordine di esecuzione del codice è dal basso verso l'alto.
A volte è possibile gestire un errore diffuso, notificato da Dimensione massima dello stack di chiamate superata. È facile ottenere questo risultato utilizzando la ricorsione:
funzione pippo() {
pippo()
}
pippo()
e il nostro browser o terminale si blocca. Ogni browser, anche nelle sue diverse versioni, ha un limite di dimensione dello stack di chiamate diverso. Nella stragrande maggioranza dei casi, sono sufficienti e il problema va cercato altrove.
Stack di chiamate bloccate
Ecco un esempio di blocco del thread JS. Proviamo a leggere un file pippo e un file bar utilizzando il NodoFunzione sincrona .js readFileSync.
Si tratta di una GIF in loop. Come si vede, il motore JS attende fino alla prima chiamata in readFileSync è completata. Ma questo non accadrà perché non c'è pippo quindi la seconda funzione non verrà mai chiamata.
Comportamento asincrono
Tuttavia, JS può anche essere non bloccante e comportarsi come se fosse multi-thread. Ciò significa che non attende la risposta di una chiamata API, eventi di I/O, ecc. e può continuare l'esecuzione del codice. Questo è possibile grazie ai motori JS che utilizzano (sotto il cofano) linguaggi multi-threading reali, come C++ (Chrome) o Rust (Firefox). Essi ci forniscono le API Web sotto il cofano del browser o, ad esempio, le API di I/O sotto l'Node.js. API I/O sotto l'Node.js.
Nella GIF qui sopra, si può vedere che la prima funzione viene spinta nello stack delle chiamate e Ciao viene eseguito immediatamente nella console.
Quindi, chiamiamo il setTimeout fornita dalla WebAPI del browser. Va allo stack delle chiamate e al suo callback asincrono pippo passa alla coda della WebApi, dove attende la chiamata, impostata dopo 3 secondi.
Nel frattempo, il programma continua il codice e si vede Salve, non sono bloccato nella console.
Dopo essere stata invocata, ogni funzione nella coda di WebAPI passa alla cartella Coda di richiamo. È il luogo in cui le funzioni attendono che lo stack delle chiamate sia vuoto. Quando ciò accade, vengono spostate lì una per una.
Quindi, quando il nostro setTimeout il timer termina il conto alla rovescia, il nostro pippo va alla coda di callback, attende che lo stack di chiamate sia disponibile, vi va, viene eseguito e vediamo Ciao da callback asincrono nella console.
Ciclo di eventi
La domanda è: come fa il runtime a sapere che lo stack di chiamate è vuoto e come viene invocato l'evento nella coda di callback? Ecco il ciclo degli eventi. È una parte del motore JS. Questo processo controlla costantemente se lo stack di chiamate è vuoto e, in caso affermativo, controlla se c'è un evento nella coda di callback in attesa di essere invocato.
Questa è tutta la magia che c'è dietro le quinte!
Conclusione della teoria
Concorrenza e parallelismo
Concorrenza significa eseguire più attività contemporaneamente, ma non simultaneamente. Ad esempio, due compiti vengono eseguiti in periodi di tempo sovrapposti.
Parallelismo significa eseguire due o più compiti contemporaneamente, ad esempio eseguire più calcoli allo stesso tempo.
Fili e processi
Fili sono una sequenza di esecuzioni di codice che possono essere eseguite indipendentemente l'una dall'altra.
Processo è un'istanza di un programma in esecuzione. Un programma può avere più processi.
Sincrono e asincrono
In sincrono Nella programmazione, i task vengono eseguiti uno dopo l'altro. Ogni compito attende il completamento del compito precedente e viene eseguito solo allora.
In asincrono Quando un'attività viene eseguita, è possibile passare a un'altra attività senza attendere il completamento di quella precedente.
Sincrono e asincrono in un ambiente singolo e multithreading
Sincrono con un singolo thread: I task vengono eseguiti uno dopo l'altro. Ogni task attende l'esecuzione del task precedente.
Sincrono con più thread: I task vengono eseguiti in thread diversi, ma attendono l'esecuzione di qualsiasi altro task su qualsiasi altro thread.
Asincrono con un singolo thread: I task iniziano a essere eseguiti senza attendere il completamento di un altro task. In un determinato momento, può essere eseguito solo un singolo task.
Asincrono con più thread: I task vengono eseguiti in thread diversi senza attendere il completamento di altri task e terminano le loro esecuzioni in modo indipendente.
Classificazione JavaScript
Se consideriamo come funziona il motore JS sotto il cofano, possiamo classificare JS come un linguaggio interpretato asincrono e a thread singolo. La parola "interpretato" è molto importante perché significa che il linguaggio sarà sempre dipendente dal tempo di esecuzione e non sarà mai veloce come i linguaggi compilati con multi-threading integrato.
È da notare che l'Node.js può realizzare un vero multi-threading, a condizione che ogni thread venga avviato come processo separato. Esistono librerie per questo, ma l'Node.js ha una funzione integrata chiamata Fili di lavoro.
Tutte le GIF del ciclo dell'evento provengono dal file Lentino creata da Philip Roberts, dove è possibile testare i propri scenari asincroni.