In che modo Node.js è intrinsecamente più veloce quando si basa ancora sui thread internamente?


281

Ho appena visto il seguente video: Introduzione a Node.js e ancora non capisco come ottenere i vantaggi della velocità.

Principalmente, a un certo punto Ryan Dahl (creatore di Node.js) afferma che Node.js è basato su loop di eventi anziché su thread. I thread sono costosi e devono essere lasciati solo agli esperti di programmazione concorrente da utilizzare.

Successivamente, mostra lo stack di architettura di Node.js che ha un'implementazione C sottostante che ha il proprio pool di thread internamente. Quindi ovviamente gli sviluppatori di Node.js non darebbero mai il via ai propri thread o non utilizzerebbero direttamente il pool di thread ... usano callback asincroni. Questo ho capito.

Quello che non capisco è il punto che Node.js sta ancora usando i thread ... sta solo nascondendo l'implementazione, quindi come è più veloce se 50 persone richiedono 50 file (non attualmente in memoria), quindi non sono necessari 50 thread ?

L'unica differenza è che, poiché è gestito internamente, lo sviluppatore Node.js non deve codificare i dettagli del thread, ma al di sotto continua a utilizzare i thread per elaborare le richieste di file IO (blocco).

Quindi non stai davvero prendendo solo un problema (threading) e nascondendolo mentre quel problema esiste ancora: principalmente thread multipli, cambio di contesto, dead-lock ... ecc?

Devono esserci alcuni dettagli che ancora non capisco qui.


14
Sono propenso a concordare con te sul fatto che l'affermazione è in qualche modo troppo semplificata. Credo che il vantaggio in termini di prestazioni del nodo si riduca a due cose: 1) i thread effettivi sono tutti contenuti a un livello abbastanza basso e rimangono quindi limitati in termini di dimensioni e numero e la sincronizzazione dei thread è quindi semplificata; 2) La "commutazione" a livello di sistema operativo tramite select()è più veloce degli scambi di contesto di thread.
Pointy,

Si prega di consultare questo stackoverflow.com/questions/24796334/…
veritas

Risposte:


140

In realtà ci sono alcune cose diverse che si confondono qui. Ma inizia con il meme che i thread sono davvero difficili. Quindi, se sono difficili, è più probabile che quando si utilizzano i thread per 1) interrompere a causa di bug e 2) non utilizzarli nel modo più efficiente possibile. (2) è quello di cui stai chiedendo.

Pensa a uno degli esempi che fornisce, in cui arriva una richiesta ed esegui una query, quindi fai qualcosa con i risultati di ciò. Se lo scrivi in ​​modo procedurale standard, il codice potrebbe apparire così:

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

Se la richiesta in arrivo ti ha fatto creare un nuovo thread che eseguiva il codice sopra, avrai un thread seduto lì, senza fare nulla mentre query()è in esecuzione. (Apache, secondo Ryan, sta usando un singolo thread per soddisfare la richiesta originale mentre nginx lo sta superando nei casi di cui parla perché non lo è.)

Ora, se fossi davvero intelligente, esprimeresti il ​​codice sopra in un modo in cui l'ambiente potrebbe spegnersi e fare qualcos'altro mentre esegui la query:

query( statement: "select smurfs from some_mushroom", callback: go_do_something_with_result() );

Questo è fondamentalmente ciò che sta facendo node.js. Fondamentalmente stai decorando - in un modo che è conveniente a causa del linguaggio e dell'ambiente, quindi dei punti sulle chiusure - il tuo codice in modo tale che l'ambiente possa essere intelligente su ciò che viene eseguito e quando. In questo modo, node.js non è nuovo nel senso che ha inventato l'I / O asincrono (non che qualcuno abbia rivendicato qualcosa del genere), ma è nuovo in quanto il modo in cui è espresso è un po 'diverso.

Nota: quando dico che l'ambiente può essere intelligente su ciò che viene eseguito e quando, in particolare ciò che intendo è che il thread utilizzato per avviare alcuni I / O ora può essere utilizzato per gestire qualche altra richiesta o un calcolo che può essere fatto in parallelo o avviare qualche altro I / O parallelo. (Non sono certo che il nodo sia abbastanza sofisticato per iniziare più lavoro per la stessa richiesta, ma hai capito.)


6
Ok, posso sicuramente vedere come questo può aumentare le prestazioni perché mi sembra che tu sia in grado di massimizzare la tua CPU perché non ci sono thread o stack di esecuzione che aspettano solo che IO ritorni, quindi ciò che Ryan ha fatto si trova effettivamente un modo per colmare tutte le lacune.
Ralph Caraveo,

34
Sì, l'unica cosa che direi è che non è come se avesse trovato un modo per colmare le lacune: non è un nuovo modello. La cosa diversa è che sta usando Javascript per consentire al programmatore di esprimere il proprio programma in un modo molto più conveniente per questo tipo di asincronia. Forse un dettaglio
nitido

16
Vale anche la pena sottolineare che per molte attività di I / O, Node utilizza qualsiasi API I / O asincrona a livello di kernel disponibile (epoll, kqueue, / dev / poll, qualunque)
Paul,

7
Non sono ancora sicuro di averlo compreso appieno. Se consideriamo che all'interno di una richiesta Web le operazioni IO sono quelle che impiegano la maggior parte del tempo necessario per elaborare la richiesta e se per ogni operazione IO viene creato un nuovo thread, quindi per 50 richieste che arrivano in una successione molto veloce, lo faremo probabilmente hanno 50 thread in esecuzione in parallelo ed eseguono la loro parte IO. La differenza dai server Web standard è che al suo interno l'intera richiesta viene eseguita sul thread, mentre in node.js solo la sua parte IO, ma quella è la parte che impiega la maggior parte del tempo e fa attendere che il thread sia in attesa.
Florin Dumitrescu,

13
@SystemParadox grazie per averlo sottolineato. Ultimamente ho fatto alcune ricerche sull'argomento e in effetti il ​​problema è che l'I / O asincrono, se correttamente implementato a livello di kernel, non usa i thread durante le operazioni di I / O asincrono. Il thread chiamante viene invece rilasciato non appena viene avviata un'operazione I / O e viene eseguita una richiamata al termine dell'operazione I / O e un thread è disponibile per essa. Quindi node.js può eseguire 50 richieste simultanee con 50 operazioni I / O in (quasi) parallelo usando solo un thread se il supporto asincrono per le operazioni I / O è implementato correttamente.
Florin Dumitrescu,

32

Nota! Questa è una vecchia risposta. Sebbene sia ancora vero nella struttura approssimativa, alcuni dettagli potrebbero essere cambiati a causa del rapido sviluppo di Node negli ultimi anni.

Sta usando i thread perché:

  1. L' opzione O_NONBLOCK di open () non funziona sui file .
  2. Esistono librerie di terze parti che non offrono IO non bloccanti.

Per simulare IO non bloccanti, i thread sono necessari: bloccare IO in un thread separato. È una brutta soluzione e causa molte spese generali.

È ancora peggio a livello hardware:

  • Con DMA la CPU scarica in modo asincrono IO.
  • I dati vengono trasferiti direttamente tra l'IO Device e la memoria.
  • Il kernel lo avvolge in una chiamata di sistema sincrona e bloccante.
  • Node.js racchiude la chiamata di sistema bloccante in un thread.

Questo è semplicemente stupido e inefficiente. Ma funziona almeno! Possiamo goderci Node.js perché nasconde i dettagli brutti e ingombranti dietro un'architettura asincrona guidata dagli eventi.

Forse qualcuno implementerà O_NONBLOCK per i file in futuro? ...

Modifica: ne ho discusso con un amico e mi ha detto che un'alternativa ai thread è il polling con select : specifica un timeout di 0 e fai IO sui descrittori di file restituiti (ora che è garantito che non si blocchino).


Che mi dici di Windows?
Pacerier,

Scusa, non ne ho idea. So solo che libuv è il livello neutro della piattaforma per fare lavori asincroni. All'inizio di Node non c'era libuv. Quindi è stato deciso di dividere libuv e questo ha reso più semplice il codice specifico della piattaforma. In altre parole, Windows ha una sua storia asincrona che potrebbe essere completamente diversa da Linux, ma per noi non importa perché libuv fa il duro lavoro per noi.
nalply

28

Temo di "fare la cosa sbagliata" qui, in tal caso cancellami e mi scuso. In particolare, non riesco a vedere come creo le piccole annotazioni ordinate che alcune persone hanno creato. Tuttavia, ho molte preoccupazioni / osservazioni da formulare su questo thread.

1) L'elemento commentato nello pseudo-codice in una delle risposte popolari

result = query( "select smurfs from some_mushroom" );
// twiddle fingers
go_do_something_with_result( result );

è essenzialmente falso. Se il thread sta elaborando, allora non è il pollice che gira, sta facendo il lavoro necessario. Se, d'altra parte, sta semplicemente aspettando il completamento di IO, allora non lo è utilizzare il tempo di CPU, il punto di tutta l'infrastruttura di controllo filo nel kernel è che la CPU troverà qualcosa di utile da fare. L'unico modo per "modificare i pollici" come suggerito qui sarebbe quello di creare un ciclo di polling, e nessuno che abbia codificato un vero server web è abbastanza inetto per farlo.

2) "I thread sono difficili", ha senso solo nel contesto della condivisione dei dati. Se hai essenzialmente thread indipendenti come nel caso della gestione di richieste web indipendenti, il threading è banalmente semplice, basta codificare il flusso lineare di come gestire un lavoro e rimanere abbastanza sapendo che gestirà più richieste, e ognuna sarà effettivamente indipendente. Personalmente, mi sarei azzardato a dire che per la maggior parte dei programmatori, apprendere il meccanismo di chiusura / callback è più complesso della semplice codifica della versione thread dall'alto verso il basso. (Ma sì, se devi comunicare tra i thread, la vita diventa davvero difficile molto velocemente, ma poi non sono convinto che il meccanismo di chiusura / callback lo cambi davvero, limita solo le tue opzioni, perché questo approccio è ancora realizzabile con i thread Comunque, quello "

3) Finora nessuno ha presentato prove concrete del perché un particolare tipo di cambio di contesto richiederebbe più o meno tempo rispetto a qualsiasi altro tipo. La mia esperienza nella creazione di kernel multi-tasking (su piccola scala per controller embedded, niente di così fantasioso come un sistema operativo "reale") suggerisce che non sarebbe così.

4) Tutte le illustrazioni che ho visto fino ad oggi che pretendono di mostrare quanto Node sia molto più veloce di altri server web sono orribilmente imperfette, tuttavia sono imperfette in un modo che illustra indirettamente un vantaggio che accetterei sicuramente per Node (e non è affatto insignificante). Il nodo non sembra che abbia bisogno (o addirittura non permetta, in realtà) di ottimizzazione. Se si dispone di un modello con thread, è necessario creare thread sufficienti per gestire il carico previsto. Fallo male e finirai con scarse prestazioni. Se ci sono troppi thread, la CPU è inattiva, ma non è in grado di accettare più richieste, creare troppi thread e sprecherai la memoria del kernel e, nel caso di un ambiente Java, sprecherai anche la memoria heap principale . Ora, per Java, sprecare heap è il primo, il migliore, modo per rovinare le prestazioni del sistema, perché un'efficiente raccolta dei rifiuti (attualmente, questo potrebbe cambiare con G1, ma sembra che la giuria sia ancora fuori su quel punto almeno all'inizio del 2013) dipende dall'avere un sacco di mucchio di riserva. Quindi, c'è il problema, ottimizzalo con troppi thread, hai CPU inattive e scarsa produttività, sintonizzalo con troppi e si impantana in altri modi.

5) Esiste un altro modo in cui accetto la logica dell'affermazione secondo cui l'approccio di Node "è più veloce in base alla progettazione", ed è questo. La maggior parte dei modelli di thread utilizza un modello di interruttore di contesto suddiviso in intervalli di tempo, sovrapposto al modello preventivo più appropriato (avviso di valutazione del valore :) e più efficiente (non un giudizio di valore). Ciò accade per due motivi, in primo luogo, la maggior parte dei programmatori non sembra comprendere la prevenzione preventiva, e in secondo luogo, se impari il threading in un ambiente Windows, il timelicing è lì che ti piaccia o no (ovviamente, questo rinforza il primo punto ; in particolare, le prime versioni di Java utilizzavano la prelazione prioritaria sulle implementazioni di Solaris e il timelicing in Windows, poiché la maggior parte dei programmatori non capiva e si lamentava che "il threading non funziona in Solaris" hanno cambiato il modello in timeslice ovunque). Comunque, la linea di fondo è che il timeslicing crea ulteriori cambi di contesto (e potenzialmente non necessari). Ogni cambio di contesto richiede tempo CPU e quel tempo viene effettivamente rimosso dal lavoro che può essere svolto sul lavoro reale a portata di mano. Tuttavia, la quantità di tempo investita nel cambio di contesto a causa del timeslicing non dovrebbe essere superiore a una percentuale molto piccola del tempo complessivo, a meno che non stia accadendo qualcosa di piuttosto stravagante e non c'è motivo per cui mi possa aspettare che ciò avvenga in un server web semplice). Quindi, sì, gli interruttori di contesto in eccesso coinvolti nel timeslicing sono inefficienti (e questi non si verificano in e quel tempo viene effettivamente rimosso dal lavoro che può essere svolto sul lavoro reale a portata di mano. Tuttavia, la quantità di tempo investita nel cambio di contesto a causa del timeslicing non dovrebbe essere superiore a una percentuale molto piccola del tempo complessivo, a meno che non stia accadendo qualcosa di piuttosto stravagante e non c'è motivo per cui mi possa aspettare che ciò avvenga in un server web semplice). Quindi, sì, gli interruttori di contesto in eccesso coinvolti nel timeslicing sono inefficienti (e questi non si verificano in e quel tempo viene effettivamente rimosso dal lavoro che può essere svolto sul lavoro reale a portata di mano. Tuttavia, la quantità di tempo investita nel cambio di contesto a causa del timeslicing non dovrebbe essere superiore a una percentuale molto piccola del tempo complessivo, a meno che non stia accadendo qualcosa di piuttosto stravagante e non c'è motivo per cui mi possa aspettare che ciò avvenga in un server web semplice). Quindi, sì, gli interruttori di contesto in eccesso coinvolti nel timeslicing sono inefficienti (e questi non si verificano inthread del kernel come regola, tra l'altro), ma la differenza sarà di qualche percento della velocità effettiva, non del tipo di fattori di numero intero impliciti nelle dichiarazioni di prestazione che sono spesso implicate per Node.

Ad ogni modo, mi scuso per tutto ciò che è lungo e sconclusionato, ma lo sento davvero finora, la discussione non ha dimostrato nulla e sarei felice di sentire qualcuno in una di queste situazioni:

a) una vera spiegazione del perché Node dovrebbe essere migliore (al di là dei due scenari che ho delineato sopra, il primo dei quali (scarsa sintonizzazione) credo sia la vera spiegazione di tutti i test che ho visto finora. ([modifica ], in realtà, più ci penso, più mi chiedo se la memoria utilizzata da un gran numero di stack potrebbe essere significativa qui. Le dimensioni di stack predefinite per i thread moderni tendono ad essere piuttosto enormi, ma la memoria allocata da un sistema di eventi basato sulla chiusura sarebbe solo ciò che è necessario)

b) un vero benchmark che offra effettivamente una buona possibilità al server threaded di scelta. Almeno in questo modo, dovrei smettere di credere che le affermazioni siano essenzialmente false;> ([modifica] probabilmente è piuttosto più forte di quanto pensassi, ma ritengo che le spiegazioni fornite per i vantaggi in termini di prestazioni siano nella migliore delle ipotesi incomplete, e il i benchmark mostrati sono irragionevoli).

Saluti, Toby


2
Un problema con i thread: hanno bisogno di RAM. Un server molto occupato può eseguire fino a qualche migliaio di thread. Node.js evita i thread ed è quindi più efficiente. L'efficienza non è eseguendo il codice più velocemente. Non importa se il codice viene eseguito nei thread o in un loop di eventi. Per la CPU è lo stesso. Ma eliminando i thread salviamo la RAM: solo uno stack anziché poche migliaia di stack. E salviamo anche i cambi di contesto.
nalply,

3
Ma il nodo non sta eliminando i thread. Li utilizza ancora internamente per le attività di I / O, che è ciò che richiede la maggior parte delle richieste Web.
levi

1
Inoltre il nodo memorizza le chiusure dei callback nella RAM, quindi non riesco a vedere dove vince.
Oleksandr Papchenko,

@levi Ma nodejs non usa il genere di cose "un thread per richiesta". Utilizza un threadpool IO, probabilmente per evitare la complicazione con l'utilizzo di API IO asincrone (e forse POSIX open()non può essere reso non bloccante?). In questo modo, ammortizza qualsiasi hit di performance in cui il modello tradizionale fork()/ pthread_create()su richiesta dovrebbe creare e distruggere i thread. E, come menzionato in Postscript a), anche questo ammortizza il problema dello spazio dello stack. Probabilmente puoi servire migliaia di richieste con, diciamo, ben 16 thread IO.
binki,

"Le dimensioni dello stack predefinite per i thread moderni tendono ad essere piuttosto grandi, ma la memoria allocata da un sistema di eventi basato sulla chiusura sarebbe solo ciò che è necessario" Ho l'impressione che dovrebbero essere dello stesso ordine. Le chiusure non sono economiche, il runtime dovrà tenere in memoria l'intero albero delle chiamate dell'applicazione a thread singolo ("stack di emulazione" per così dire) e sarà in grado di ripulire quando una foglia di albero viene rilasciata come chiusura associata viene "risolto". Ciò includerà molti riferimenti a elementi on-heap che non possono essere raccolti in modo inutile e influiranno sulle prestazioni al momento della pulizia.
David Tonhofer,

14

Quello che non capisco è il punto che Node.js sta ancora usando i thread.

Ryan usa i thread per quelle parti che stanno bloccando (la maggior parte di node.js usa IO non bloccanti) perché alcune parti sono pazzesche difficili da scrivere senza bloccare. Ma credo che Ryan desideri avere tutto ciò che non blocca. Nella diapositiva 63 (design interno) viene visualizzato Ryan che utilizza libev (libreria che estrae la notifica di eventi asincroni) per il eventloop non bloccante . A causa del loop degli eventi node.js necessita di thread minori che riducono il cambio di contesto, il consumo di memoria ecc.


11

I thread sono usati solo per gestire funzioni che non hanno strutture asincrone, come stat().

La stat()funzione è sempre bloccante, quindi node.js deve utilizzare un thread per eseguire la chiamata effettiva senza bloccare il thread principale (loop degli eventi). Potenzialmente, nessun thread dal pool di thread verrà mai utilizzato se non è necessario chiamare quel tipo di funzioni.


7

Non so nulla del funzionamento interno di node.js, ma posso vedere come l'utilizzo di un loop di eventi può superare la gestione degli I / O thread. Immagina una richiesta del disco, dammi staticFile.x, rendilo 100 richieste per quel file. Ogni richiesta occupa normalmente un thread che recupera quel file, ovvero 100 thread.

Ora immagina la prima richiesta di creazione di un thread che diventa un oggetto editore, tutte le altre 99 richieste prima guardano se c'è un oggetto editore per staticFile.x, in tal caso, ascoltalo mentre sta funzionando, altrimenti avvia un nuovo thread e quindi un nuovo oggetto editore.

Una volta terminato il singolo thread, passa staticFile.x a tutti i 100 listener e si distrugge, quindi la richiesta successiva crea un nuovo thread e un nuovo oggetto publisher.

Quindi nell'esempio sopra sono 100 thread vs 1 thread, ma anche 1 ricerca del disco anziché 100 ricerche del disco, il guadagno può essere abbastanza fenomenale. Ryan è un ragazzo intelligente!

Un altro modo di vedere è uno dei suoi esempi all'inizio del film. Invece di:

pseudo code:
result = query('select * from ...');

Ancora una volta, 100 query separate su un database contro ...:

pseudo code:
query('select * from ...', function(result){
    // do stuff with result
});

Se una query era già in corso, altre query uguali salterebbero semplicemente sul carrozzone, quindi è possibile avere 100 query in un solo roundtrip di database.


3
La questione del database è più una questione di non aspettare la risposta mentre si trattengono altre richieste (che possono o meno usare il database), ma piuttosto chiedere qualcosa e poi lasciarti chiamare quando torna. Non penso che li colleghi insieme, poiché sarebbe piuttosto difficile tenere traccia della risposta. Inoltre non credo che ci sia un'interfaccia MySQL che ti consenta di contenere più risposte senza buffer su una connessione (??)
Tor Valamo,

È solo un esempio astratto per spiegare come i loop di eventi possano offrire maggiore efficienza, nodejs non fa nulla con i DB senza moduli extra;)
BGerrissen,

1
Sì, il mio commento è stato più rivolto alle 100 query in un singolo roundtrip di database. : p
Tor Valamo,

2
Ciao BGerrissen: bel post. Quindi, quando viene eseguita una query, altre query simili "ascoltano" come nell'esempio staticFile.X sopra? ad esempio, 100 utenti recuperano la stessa query, verrà eseguita una sola query e gli altri 99 ascolteranno la prima? Grazie !
CAP

1
Stai facendo sembrare che nodejs memorizzi automaticamente le chiamate di funzione o qualcosa del genere. Ora, poiché non devi preoccuparti della sincronizzazione della memoria condivisa nel modello di loop degli eventi di JavaScript, è più facile memorizzare nella cache oggetti in memoria in modo sicuro. Ma ciò non significa che nodejs lo faccia magicamente per te o che questo sia il tipo di miglioramento delle prestazioni che ti viene chiesto.
binki,
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.