Esecuzione I / O asincrono NodeJS

Per quanto ho capito, sebbene apparentemente ci sia un thread “helper”, Node.js viene eseguito in un singolo thread, quindi, ogni operazione nello stack Loop eventi viene eseguita una dopo l’altra e altre operazioni e accodata mentre il nodo esegue l’I / O asincrono sullo sfondo, in questo modo il server è in grado di eseguire altre azioni mentre esegue l’I / O non bloccante senza la necessità di creare più thread inutili, l’I / O è terminato e il richiamo associato viene inserito nella coda del Loop degli eventi, e questo è il bello di Node.

Tuttavia, in molti articoli che ho pronto, non è chiaro se le operazioni di I / O asincrone vengano eseguite in parallelo con altre operazioni di I / O in I / O, un thread o processo separato o se ciascuna operazione I / O richiesta viene eseguita una dopo l’altra in un thread di supporto mentre l’Event Loop esegue altre azioni. E dopo aver letto la frase “Tutto funziona in parallelo tranne il tuo codice” questo mi rende ancora più confuso.

La domanda è, multithread o non multithread? Se ogni operazione asincrona viene eseguita in un thread separato, non deve utilizzare tutte le risorse richieste da un server Apache?

Il nodo è, essenzialmente, non multithreaded. L’asincronicità va più in profondità del Nodo, più profonda della libuv e persino più profonda delle strutture ( epoll , kqueue , IOCP , ecc.) Che usa la libuv.

Quando il kernel riceve una richiesta asincrona, non avvia un altro thread di esecuzione. Invece, lo aggiunge a una semplice lista di “cose ​​a cui fare attenzione”. Se un processo effettua una richiesta di lettura di rete, ad esempio, il kernel creerà una voce in tale elenco. È qualcosa come “hey, la prossima volta che c’è una richiesta di lettura che assomiglia a questo, lascia che sia il processo a saperlo.” Dopo aver effettuato questa immissione, il kernel restituisce il controllo al processo ed entrambi procedono in modo allegro. L’unica cosa che sopravvive sono i dati sulla lista.

Il kernel viene informato di un evento di lettura di rete tramite interrupt hardware. Usando un interrupt, il processore mette il kernel in un loop speciale – fermando qualsiasi cosa stia facendo al momento – e lo racconta sull’evento. Il kernel verifica quindi l’elenco delle richieste in sospeso e (nel caso di AIO kevent) invia un interrupt simile (sotto forma di segnale) al processo per informarlo della lettura della rete. Quindi, niente discussioni. Solo interruzioni.

Bene, questa è una semplificazione: nei casi non-AIO kevent ed epoll, dopo che il kernel ha letto una rete, lo inserirà in una lista eventi. Il processo controlla periodicamente l’elenco degli eventi per vedere se è arrivato qualcosa.

Inoltre, dalla vista kernel, questo è il modo in cui tutto l’I / O funziona. La grande differenza è che il kernel non richiede che il processo attenda che il kernel ritorni ad esso.

C’è un po ‘più di complessità in libuv come richieste non di rete (e le richieste DNS, che sono forms speciali e dolorose di richieste di rete) sono gestite da thread. Questo perché le funzionalità del kernel per rendere quelle asincrone non sono generalmente così grandi, se esistono.

Non è multithread con un’eccezione minore. Parliamo di questa eccezione più tardi. Innanzitutto, vediamo perché le cose possono accadere in parallelo ma non in multithreading.

In attesa di I / O

Quando si invia qualcosa attraverso la rete, ad esempio, si effettua una richiesta http, in attesa di una risposta http da completare o in attesa che mysql risponda al proprio software non sta facendo nulla.

Quindi, come può funzionare la rete se il tuo software non sta facendo nulla? (scusate se questo è ovvio ma sottolineando l’ovvio è di solito ciò che lo rende ovvio) .

Alcune cose accadono al di fuori della CPU

Innanzitutto, la maggior parte della rete è al di fuori della CPU. I dati di buffer della scheda di rete vengono inviati e ricevuti. Gli elettroni nel cavo di rete o i fotoni nello spazio / aria vibrano in base al segnale inviato dalle apparecchiature di rete. Hai il tuo router, il tuo ISP, il server dall’altra parte del pianeta ecc.

Tutto quanto sopra deve essere elaborato in modo che la tua richiesta http possa restituire i dati. Nella maggior parte delle lingue, mentre succede tutto quanto sopra, il tuo codice non farà nulla.

Non così in javascript. Quando viene fatta una richiesta di I / O, invece di aspettare che i dati tornino, l’interprete registra semplicemente una callback che fornisci per l’esecuzione quando i dati arrivano finalmente qui. Ora che è fatto, è ansible eseguire altro codice. Forse alcuni altri dati richiesti in precedenza ora sono qui e tale callback può essere eseguito. Forse un setTimeout è scaduto ed è ora di chiamare quella richiamata.

Quindi più cose possono accadere in parallelo, molte delle quali al di fuori del tuo processo, molte delle quali fuori dalla tua CPU, altre su un’altra macchina che potrebbe trovarsi anche dall’altra parte del pianeta. Mentre sta succedendo, javascript ti permette di eseguire del codice.

L’eccezione: I / O del disco

L’eccezione è l’I / O del disco. Al livello più basso (in realtà, dal prossimo al basso) C espone solo funzioni sincrone come fread() , fwrite() per I / O. Anche la lettura dei pacchetti di rete è tecnicamente sincrona. La differenza è che la rete non risponde immediatamente, quindi il codice di rete ha molto tempo da perdere in attesa di pacchetti. È tra queste letture e scritture che javascript esegue il tuo codice. Ma il filesystem ti dirà felicemente che i dati sono immediatamente disponibili. Quindi, a differenza del codice di rete, la lettura del codice da disco trascorrerà la maggior parte del tempo a essere occupata.

Ci sono diversi modi per ovviare a questo. Alcuni sistemi operativi hanno anche API asincrone per leggere dal disco. Gli sviluppatori node.js hanno deciso di gestire questa situazione creando un altro thread per eseguire I / O su disco. Quindi per I / O su disco è parallelo perché è multithread.