Come funziona il singolo modello IO non bloccante con thread in Node.js


325

Non sono un programmatore di nodi, ma sono interessato a come funziona il modello IO non bloccante a thread singolo . Dopo aver letto l'articolo capire-il-nodo-js-event-loop , sono davvero confuso al riguardo. Ha fornito un esempio per il modello:

c.query(
   'SELECT SLEEP(20);',
   function (err, results, fields) {
     if (err) {
       throw err;
     }
     res.writeHead(200, {'Content-Type': 'text/html'});
     res.end('<html><head><title>Hello</title></head><body><h1>Return from async DB query</h1></body></html>');
     c.end();
    }
);

Que: quando ci sono due richieste A (viene prima) e B poiché esiste un solo thread, il programma lato server gestirà la richiesta A in primo luogo: l'esecuzione di query SQL è un'istruzione sleep che indica l'attesa I / O. E il programma è bloccato I/Oall'attesa e non può eseguire il codice che rende la pagina web dietro. Il programma passerà alla richiesta B durante l'attesa? A mio avviso, a causa del modello a thread singolo, non è possibile passare da una richiesta a un'altra. Ma il titolo del codice di esempio dice che tutto funziona in parallelo tranne il tuo codice .

(PS Non sono sicuro di aver frainteso il codice o meno dal momento che non ho mai usato Node.) Come Nodo passa da A a B durante l'attesa? E puoi spiegare in modo semplice il modello IO non threading a thread singolo di Node? Gradirei se tu potessi aiutarmi. :)

Risposte:


374

Node.js è basato su libuv , una libreria multipiattaforma che estrae apis / syscalls per input / output asincroni (non bloccanti) forniti dai sistemi operativi supportati (almeno Unix, OS X e Windows).

IO asincrono

In questo modello di programmazione le operazioni di apertura / lettura / scrittura su dispositivi e risorse (socket, filesystem, ecc.) Gestite dal file system non bloccano il thread chiamante (come nel tipico modello sincrono simil-c) e contrassegnano semplicemente il processo (nella struttura dei dati a livello di kernel / OS) per essere avvisato quando sono disponibili nuovi dati o eventi. Nel caso di un'app simile a un web server, il processo è quindi responsabile di capire a quale richiesta / contesto appartiene l'evento notificato e procedere con l'elaborazione della richiesta da lì. Nota che questo significa necessariamente che sarai su un frame stack diverso da quello che ha originato la richiesta al sistema operativo in quanto quest'ultimo ha dovuto cedere a un dispatcher di processo affinché un singolo processo con thread potesse gestire nuovi eventi.

Il problema con il modello che ho descritto è che non è familiare e difficile da ragionare per il programmatore in quanto non di natura sequenziale. "È necessario effettuare una richiesta nella funzione A e gestire il risultato in una funzione diversa in cui i locali di A di solito non sono disponibili."

Modello del nodo (Continuing Passing Style ed Event Loop)

Node affronta il problema sfruttando le funzionalità del linguaggio javascript per rendere questo modello un po 'più sincrono inducendo il programmatore a utilizzare un certo stile di programmazione. Ogni funzione che richiede IO ha una firma simile function (... parameters ..., callback)e deve ricevere una richiamata che verrà invocata al completamento dell'operazione richiesta (tenere presente che la maggior parte del tempo è trascorso in attesa che il sistema operativo segnali il completamento - tempo che può essere trascorso altri lavori). Il supporto di Javascript per le chiusure consente di utilizzare le variabili definite nella funzione esterna (chiamata) all'interno del corpo del callback; ciò consente di mantenere lo stato tra le diverse funzioni che verranno invocate dal runtime del nodo in modo indipendente. Vedi anche Continuing Passing Style .

Inoltre, dopo aver richiamato una funzione che genera un'operazione IO, la funzione chiamante returncontrolla di solito il ciclo di eventi del nodo . Questo loop richiamerà il callback o la funzione successiva programmata per l'esecuzione (molto probabilmente perché l'evento corrispondente è stato notificato dal sistema operativo) - questo consente l'elaborazione simultanea di più richieste.

Puoi pensare al loop degli eventi del nodo in qualche modo simile al dispatcher del kernel: il kernel pianificherebbe l'esecuzione di un thread bloccato una volta completato il suo IO in sospeso mentre il nodo pianificherà un callback quando si è verificato l'evento corrispondente.

Altamente concorrente, nessun parallelismo

Come osservazione finale, la frase "tutto funziona in parallelo tranne il tuo codice" fa un lavoro decente nel catturare il punto in cui il nodo consente al tuo codice di gestire richieste da centinaia di migliaia di socket aperti con un singolo thread contemporaneamente multiplexando e sequenziando tutti i tuoi js logica in un singolo flusso di esecuzione (anche se dire "tutto corre in parallelo" probabilmente qui non è corretto - vedi Concorrenza vs Parallelismo - Qual è la differenza? ). Funziona abbastanza bene per i server webapp poiché la maggior parte del tempo viene effettivamente impiegata in attesa di rete o disco (database / socket) e la logica non richiede molta CPU, vale a dire: funziona bene per carichi di lavoro associati a IO .


45
A domande di follow-up: come avviene effettivamente l'I / O? Il nodo sta facendo una richiesta al sistema e chiede di essere avvisato al termine. Quindi il sistema sta eseguendo un thread che sta eseguendo l'I / O o sta eseguendo anche l'I / O in modo asincrono a livello hardware usando gli interrupt? Qualcosa da qualche parte deve aspettare che l'I / O finisca, e questo si bloccherà fino a quando non sarà terminato e consumerà una quantità di risorse.
Filippo,

6
Ho appena notato che questo commento di follow-up risponde a @ user568109 di seguito, vorrei che ci fosse un modo per unire queste due risposte.
lfalin,

4
Vorrei che tu potessi scrivere una risposta il doppio del tempo, quindi avrei capito il doppio meglio.
Rafael Eyng,

Il nodo è supportato in molti punti, per la cronaca. Quando stavo progettando il firmware per i router MIPS32, Node.JS poteva essere eseguito su quelli tramite OpenWRT.
Qix - MONICA È STATA MISTREATA

Come segna su Apache? Apache è anche in grado di gestire connessioni simultanee con un thread separato.
Suhail Gupta,

210

Bene, per dare una prospettiva, fammi confrontare node.js con apache.

Apache è un server HTTP multi-thread, per ogni singola richiesta ricevuta dal server, crea un thread separato che gestisce quella richiesta.

Node.js invece è guidato dagli eventi, gestendo tutte le richieste in modo asincrono dal singolo thread.

Quando A e B vengono ricevuti su apache, vengono creati due thread che gestiscono le richieste. Ognuno gestisce la query separatamente, ciascuno in attesa dei risultati della query prima di pubblicare la pagina. La pagina viene pubblicata solo fino al termine della query. Il recupero della query sta bloccando perché il server non può eseguire il resto del thread fino a quando non riceve il risultato.

Nel nodo, c.query viene gestito in modo asincrono, il che significa che mentre c.query recupera i risultati per A, salta per gestire c.query per B, e quando arrivano i risultati per A arriva restituisce i risultati al callback che invia il risposta. Node.js sa eseguire il callback al termine del recupero.

A mio avviso, poiché si tratta di un modello a thread singolo, non è possibile passare da una richiesta all'altra.

In realtà il nodo server fa esattamente questo per te tutto il tempo. Per effettuare switch, (il comportamento asincrono) la maggior parte delle funzioni che useresti avranno callback.

modificare

La query SQL è tratta dalla libreria mysql . Implementa lo stile di callback e l'emettitore di eventi per mettere in coda le richieste SQL. Non li esegue in modo asincrono, ciò viene fatto dai thread libuv interni che forniscono l'astrazione di I / O non bloccanti. I seguenti passaggi si verificano per eseguire una query:

  1. Aprire una connessione a db, la connessione stessa può essere effettuata in modo asincrono.
  2. Una volta collegato db, la query viene passata al server. Le query possono essere messe in coda.
  3. Il loop dell'evento principale viene avvisato del completamento con callback o evento.
  4. Il ciclo principale esegue il callback / eventhandler.

Le richieste in arrivo al server http vengono gestite in modo simile. L'architettura del thread interno è simile a questa:

loop di eventi node.js

I thread C ++ sono quelli libuv che eseguono l'I / O asincrono (disco o rete). Il ciclo di eventi principale continua a essere eseguito dopo l'invio della richiesta al pool di thread. Può accettare più richieste in quanto non attende o non dorme. Le query SQL / richieste HTTP / letture di file system avvengono tutte in questo modo.


16
Il diagramma è molto utile.
Anmol Saraf,

14
Aspetta, quindi nel tuo diagramma hai il "pool di thread C ++ interno", il che significa che tutte le operazioni di blocco IO genereranno un thread, giusto? Quindi, se la mia app Node funziona per qualche I / O per ogni richiesta , non c'è praticamente alcuna differenza tra il modello Node e il modello Apache? Non mi dispiace questa parte.
gav.newalkar,

21
@ gav.newalkar Non generano un thread, le richieste vengono messe in coda. I thread nel threadpool li elaborano. I thread non sono dinamici e per richiesta come in Apache. Di solito sono fissi e differiscono da sistema a sistema.
user568109,

10
@ user568109 Ma Apache utilizza anche un threadpool ( httpd.apache.org/docs/2.4/mod/worker.html ). Quindi alla fine la differenza tra un setup con node.js differisce da uno con Apache davanti solo nel punto in cui si trova il threadpool, non è vero?
Kris,

13
Tale diagramma dovrebbe trovarsi nella prima pagina dei documenti ufficiali.
bouvierr,

52

Node.js usa libuv dietro le quinte. libuv ha un pool di thread (di default 4). Pertanto Node.js utilizza i thread per raggiungere la concorrenza.

Tuttavia , il codice viene eseguito su un singolo thread (ovvero, tutti i callback delle funzioni Node.js verranno chiamati sullo stesso thread, il cosiddetto loop-thread o event-loop). Quando le persone dicono "Node.js viene eseguito su un singolo thread", in realtà stanno dicendo "i callback di Node.js vengono eseguiti su un singolo thread".


1
Risposta breve ma chiara (y)
Sudhanshu Gaur,

1
buona risposta Aggiungerei che l'I / O si verifica al di fuori di questo ciclo principale di eventi, loop-thread, thread di richiesta
Ionut Popa

questa è la risposta che stavo cercando da 2 ore, come la concorrenza è stata gestita in un'applicazione a thread singolo
Muhammad Ramzan,

sì, difficile ottenere la risposta di "livello successivo". Questo spiega dove viene effettivamente eseguito l'IO (in un pool di thread altrove)
Oliver Shaw,

9

Node.js si basa sul modello di programmazione del loop di eventi. Il ciclo di eventi viene eseguito in thread singolo, attende ripetutamente gli eventi e quindi esegue tutti i gestori di eventi sottoscritti a tali eventi. Gli eventi possono essere ad esempio

  • l'attesa del timer è completa
  • il prossimo blocco di dati è pronto per essere scritto in questo file
  • c'è una nuova richiesta HTTP in arrivo

Tutto questo viene eseguito in thread singolo e nessun codice JavaScript viene mai eseguito in parallelo. Fintanto che questi gestori di eventi sono piccoli e aspettano altri eventi, tutto funziona alla perfezione. Ciò consente di gestire più richieste contemporaneamente da un singolo processo Node.js.

(C'è un po 'di magia sotto il cofano da dove hanno origine gli eventi. Alcuni di questi coinvolgono thread di lavoro di basso livello che corrono in parallelo.)

In questo caso SQL, ci sono molte cose (eventi) che accadono tra fare la query del database e ottenere i suoi risultati nel callback . Durante quel periodo il loop degli eventi continua a pompare la vita nell'applicazione e ad avanzare altre richieste un piccolo evento alla volta. Pertanto vengono servite più richieste contemporaneamente.

visualizzazione ad alto livello del loop degli eventi

Secondo: "Loop di eventi da 10.000 piedi - concetto di base dietro Node.js" .


5

La funzione c.query () ha due argomenti

c.query("Fetch Data", "Post-Processing of Data")

L'operazione "Recupera dati" in questo caso è una query DB, ora può essere gestita da Node.js generando un thread di lavoro e assegnandogli questa attività di esecuzione della query DB. (Ricorda che Node.js può creare thread internamente). Ciò consente alla funzione di tornare istantaneamente senza alcun ritardo

Il secondo argomento "Post-Processing of Data" è una funzione di callback, il framework del nodo registra questo callback e viene chiamato dal loop degli eventi.

Pertanto, l'istruzione c.query (paramenter1, parameter2)tornerà istantaneamente, consentendo al nodo di soddisfare un'altra richiesta.

PS: Ho appena iniziato a capire il nodo, in realtà volevo scrivere questo come commento a @Philip ma dal momento che non aveva abbastanza punti reputazione quindi l'ho scritto come una risposta.


3

se leggi ancora un po '- "Ovviamente, sul backend, ci sono thread e processi per l'accesso al DB e l'esecuzione del processo. Tuttavia, questi non sono esplicitamente esposti al tuo codice, quindi non puoi preoccuparti di loro se non conoscendo che le interazioni di I / O, ad esempio con il database o con altri processi, saranno asincrone dal punto di vista di ogni richiesta poiché i risultati di tali thread vengono restituiti tramite il ciclo degli eventi al codice. "

about - "tutto funziona in parallelo tranne il tuo codice" - il tuo codice viene eseguito in modo sincrono, ogni volta che invochi un'operazione asincrona come aspettare IO, il loop degli eventi gestisce tutto e richiama il callback. non è qualcosa a cui devi pensare.

nel tuo esempio: ci sono due richieste A (viene prima) e B. esegui la richiesta A, il tuo codice continua a essere eseguito in modo sincrono ed esegue la richiesta B. il ciclo di eventi gestisce la richiesta A, quando termina invoca il callback della richiesta A con il risultato, lo stesso vale per la richiesta B.


3
"Certo, sul backend, ci sono thread e processi per l'accesso al DB e l'esecuzione del processo. Tuttavia, questi non sono esplicitamente esposti al tuo codice" - Se prendo da questa frase, allora non vedo alcuna differenza tra ciò che Nodo fare o qualsiasi framework multithread - diciamo Java Spring Framework - lo fa. Ci sono discussioni, ma non controlli la loro creazione.
Rafael Eyng,

@RafaelEyng Penso che per gestire la serie di richieste multiple, il nodo avrà sempre un singolo thread per quello. Non sono sicuro se ogni callback viene inserito in una nuova istanza di thread oltre ad altri processi come l'accesso a db ma almeno sappiamo sicuramente che il nodo non crea un'istanza di thread ogni volta che riceve una richiesta che dovrà attendere in linea prima dell'elaborazione (esecuzioni prima il callback).
Cold Cerberus,

1

Va bene, la maggior parte delle cose dovrebbe essere chiara finora ... la parte difficile è l'SQL : se in realtà non è in esecuzione in un altro thread o processo nella sua interezza, l'esecuzione di SQL deve essere suddivisa in singoli passaggi (da un Processore SQL realizzato per l'esecuzione asincrona!), Dove vengono eseguiti quelli non bloccanti e quelli bloccanti (ad esempio lo sleep) possono essere effettivamente trasferiti al kernel (come un allarme / interruzione di allarme) e inseriti nell'elenco degli eventi per il anello principale.

Ciò significa che, ad esempio, l'interpretazione dell'SQL, ecc. Viene eseguita immediatamente, ma durante l'attesa (memorizzata come un evento in futuro dal kernel in una struttura kqueue, epoll, ...; insieme alle altre operazioni di I / O ) il loop principale può fare altre cose e infine verificare se è successo qualcosa di tali IO e attende.

Quindi, per riformularlo di nuovo: il programma non è mai (permesso di rimanere bloccato), le chiamate in sospensione non vengono mai eseguite. Il loro compito è svolto dal kernel (scrivere qualcosa, attendere che qualcosa entri nella rete, aspettando che scada il tempo) o un altro thread o processo. - Il processo Node verifica se almeno una di queste funzioni è terminata dal kernel nell'unica chiamata di blocco al sistema operativo una volta in ciascun ciclo di eventi. Quel punto viene raggiunto quando viene fatto tutto il non-blocco.

Chiaro? :-)

Non conosco Node. Ma da dove proviene la query c.?


kqueue epoll è per la notifica di I / O asincroni scalabili nel kernel linux. Nodo ha libuv per quello. Il nodo è interamente su userland. Non dipende da cosa implementa il kernel.
user568109

1
@ user568109, libuv è l'uomo di mezzo di Node. Qualsiasi framework asincrono dipende (direttamente o meno) da un supporto I / O asincrono nel kernel. Così?
Robert Siemer,

Dispiace per la confusione. Le operazioni socket richiedono I / O non bloccanti dal kernel. Si occupa della gestione asincrona. Ma l'I / O asincrono dei file è gestito dallo stesso libuv. La tua risposta non lo dice. Tratta entrambi allo stesso modo, essendo gestito dal kernel.
user568109
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.