Quando viene utilizzato il pool di thread?


104

Quindi ho una comprensione di come funziona Node.js: ha un singolo thread listener che riceve un evento e quindi lo delega a un pool di lavoro. Il thread di lavoro notifica al listener una volta completato il lavoro e il listener restituisce quindi la risposta al chiamante.

La mia domanda è questa: se alzo un server HTTP in Node.js e chiamo sleep su uno dei miei eventi di percorso instradato (come "/ test / sleep"), l'intero sistema si ferma. Anche il singolo thread dell'ascoltatore. Ma la mia comprensione era che questo codice sta accadendo nel pool di lavoro.

Ora, al contrario, quando uso Mongoose per parlare con MongoDB, le letture DB sono un'operazione di I / O costosa. Il nodo sembra essere in grado di delegare il lavoro a un thread e ricevere il callback quando viene completato; il tempo impiegato per il caricamento dal DB non sembra bloccare il sistema.

In che modo Node.js decide di utilizzare un thread del pool di thread rispetto al thread del listener? Perché non riesco a scrivere codice evento che dorme e blocchi solo un thread del pool di thread?


@Tobi - L'ho visto. Ancora non risponde alla mia domanda. Se il lavoro fosse su un altro thread, il sonno interesserebbe solo quel thread e non anche l'ascoltatore.
Haney

8
Una domanda genuina, dove cerchi di capire qualcosa da solo, e quando non riesci a trovare un'uscita per il labirinto, chiedi aiuto.
Rafael Eyng

Risposte:


241

La tua comprensione di come funziona il nodo non è corretta ... ma è un'idea sbagliata comune, perché la realtà della situazione è in realtà piuttosto complessa e tipicamente ridotta a piccole frasi concise come "il nodo è a thread singolo" che semplifica eccessivamente le cose .

Per il momento, ignoreremo il multi-elaborazione / multi-threading esplicito tramite cluster e thread di webworker e parleremo solo del tipico nodo non threaded.

Il nodo viene eseguito in un singolo ciclo di eventi. È a thread singolo e ottieni sempre quell'unico thread. Tutto il javascript che scrivi viene eseguito in questo ciclo e se un'operazione di blocco si verifica in quel codice, bloccherà l'intero ciclo e nient'altro accadrà fino al termine. Questa è la natura tipicamente a thread singolo del nodo di cui si sente tanto parlare. Ma non è l'intero quadro.

Alcune funzioni e moduli, solitamente scritti in C / C ++, supportano l'I / O asincrono. Quando chiami queste funzioni e metodi, gestiscono internamente il passaggio della chiamata a un thread di lavoro. Ad esempio, quando si utilizza il fsmodulo per richiedere un file, il fsmodulo passa quella chiamata a un thread di lavoro e quel worker attende la sua risposta, che quindi presenta di nuovo al ciclo di eventi che è stato agitato senza di esso nel frattempo. Tutto questo viene estratto da te, lo sviluppatore del nodo, e parte di esso viene astratto dagli sviluppatori del modulo attraverso l'uso di libuv .

Come sottolineato da Denis Dollfus nei commenti (da questa risposta a una domanda simile), la strategia utilizzata da libuv per ottenere I / O asincrono non è sempre un pool di thread, in particolare nel caso del httpmodulo sembra essere una strategia diversa utilizzato in questo momento. Per i nostri scopi qui è principalmente importante notare come si ottiene il contesto asincrono (utilizzando libuv) e che il pool di thread mantenuto da libuv è una delle molteplici strategie offerte da quella libreria per ottenere l'asincronicità.


Su una tangente per lo più correlata, c'è un'analisi molto più approfondita di come il nodo raggiunge l'asincronicità, e alcuni potenziali problemi correlati e come affrontarli, in questo eccellente articolo . La maggior parte si espande su ciò che ho scritto sopra, ma in aggiunta sottolinea:

  • È probabile che qualsiasi modulo esterno che includi nel tuo progetto che fa uso di C ++ nativo e libuv utilizzi il pool di thread (pensa: accesso al database)
  • libuv ha una dimensione predefinita del pool di thread di 4 e utilizza una coda per gestire l'accesso al pool di thread: il risultato è che se hai 5 query DB di lunga durata tutte in esecuzione contemporaneamente, una di esse (e qualsiasi altra asincrona azione che si basa sul pool di thread) aspetterà il completamento di tali query prima ancora che vengano avviate
  • Puoi mitigare questo problema aumentando la dimensione del pool di thread tramite la UV_THREADPOOL_SIZEvariabile di ambiente, purché lo fai prima che il pool di thread sia richiesto e creato:process.env.UV_THREADPOOL_SIZE = 10;

Se desideri il multi-processing tradizionale o il multi-threading in node, puoi ottenerlo attraverso il clustermodulo integrato o vari altri moduli come il suddetto webworker-threads, oppure puoi fingere implementando un modo per suddividere il tuo lavoro e manualmente usando setTimeouto setImmediateo process.nextTickper mettere in pausa il lavoro e continuarlo in un ciclo successivo per consentire il completamento di altri processi (ma non è consigliato).

Tieni presente che se stai scrivendo codice di lunga durata / blocco in javascript, probabilmente stai commettendo un errore. Altre lingue funzioneranno in modo molto più efficiente.


1
Santo cielo, questo chiarisce completamente tutto per me. Grazie mille @ Jason!
Haney

5
Nessun problema :) Mi sono trovato dove sei non molto tempo fa, ed è stato difficile arrivare a una risposta ben definita perché da un lato hai sviluppatori C / C ++ per i quali la risposta è ovvia, e dall'altro hai tipico sviluppatori web che non hanno mai approfondito questo tipo di domande prima d'ora. Non sono nemmeno sicuro che la mia risposta sia tecnicamente corretta al 100% quando scendi al livello C, ma è giusta a grandi linee.
Jason

3
L'utilizzo del pool di thread per le richieste di rete sarebbe un enorme spreco di risorse. Secondo questa domanda "Esegue l'I / O di rete asincrono in base alle interfacce I / O asincrone in piattaforme diverse, come epoll, kqueue e IOCP, senza un pool di thread" - il che ha senso.
Denis Dollfus

1
... detto questo, se esegui un lavoro pesante direttamente nel thread javascript principale, o non hai abbastanza risorse o non le gestisci in modo appropriato per dare abbastanza margine al threadpool, potresti introdurre lag a una concorrenza inferiore soglia: il risultato è che, per le stesse risorse di sistema, in genere sperimenterai un thruput più elevato con node.js che con altre opzioni (sebbene ci siano altri sistemi basati su eventi in altre lingue che mirano a sfidarlo - non l'ho fatto visto i benchmark recenti) - è chiaro che un modello basato su eventi supera un modello a thread.
Jason

1
@Aabid Il thread listener non esegue una query sul database, quindi ci vorranno circa 6 secondi per completare tutte e 10 queste query (per la dimensione predefinita del pool di thread di 4). Se devi eseguire qualsiasi lavoro in javascript che non richiede il completamento dei risultati di quella query sul database, ad esempio arrivano più richieste che non richiedono alcun lavoro asincrono per essere completato dal pool di thread, continuerà a funzionare nella principale loop di eventi.
Jason

20

Quindi ho una comprensione di come funziona Node.js: ha un singolo thread listener che riceve un evento e quindi lo delega a un pool di lavoro. Il thread di lavoro notifica al listener una volta completato il lavoro e il listener restituisce quindi la risposta al chiamante.

Questo non è veramente accurato. Node.js ha un solo thread "worker" che esegue l'esecuzione di javascript. Ci sono thread all'interno del nodo che gestiscono l'elaborazione di I / O, ma pensarli come "lavoratori" è un'idea sbagliata. In realtà ci sono solo la gestione dell'IO e pochi altri dettagli dell'implementazione interna del nodo, ma come programmatore non puoi influenzare il loro comportamento se non alcuni parametri vari come MAX_LISTENERS.

La mia domanda è questa: se alzo un server HTTP in Node.js e chiamo sleep su uno dei miei eventi di percorso instradato (come "/ test / sleep"), l'intero sistema si ferma. Anche il singolo thread dell'ascoltatore. Ma la mia comprensione era che questo codice sta accadendo nel pool di lavoro.

Non esiste un meccanismo di sospensione in JavaScript. Potremmo discuterne più concretamente se pubblicaste uno snippet di codice di ciò che pensate significhi "dormire". Non esiste una tale funzione da chiamare per simulare qualcosa come time.sleep(30)in Python, per esempio. C'è, setTimeoutma fondamentalmente NON è dormire. setTimeoute rilasciaresetInterval esplicitamente , non bloccare, il ciclo di eventi in modo che altri bit di codice possano essere eseguiti sul thread di esecuzione principale. L'unica cosa che puoi fare è occupare il loop della CPU con il calcolo in memoria, che in effetti farà morire di fame il thread di esecuzione principale e renderà il tuo programma non rispondente.

In che modo Node.js decide di utilizzare un thread del pool di thread rispetto al thread del listener? Perché non riesco a scrivere codice evento che dorme e blocchi solo un thread del pool di thread?

L'I / O di rete è sempre asincrono. Fine della storia. Disk IO ha API sincrone e asincrone, quindi non c'è alcuna "decisione". node.js si comporterà in base alle funzioni principali dell'API che chiami sync rispetto al normale async. Ad esempio: fs.readFilevs fs.readFileSync. Per processi figli, ci sono anche separati child_process.exece child_process.execSyncle API.

La regola pratica è utilizzare sempre le API asincrone. I motivi validi per utilizzare le API di sincronizzazione sono per il codice di inizializzazione in un servizio di rete prima che sia in ascolto di connessioni o in script semplici che non accettano richieste di rete per strumenti di compilazione e quel genere di cose.


1
Da dove provengono queste API asincrone? Ho capito cosa stai dicendo, ma chiunque abbia scritto queste API ha optato per IOCP / async. Come hanno scelto di farlo?
Haney

3
La sua domanda è come scriverebbe il suo codice che richiede molto tempo e non il blocco.
Jason

1
Sì. Il nodo fornisce reti UDP, TCP e HTTP di base. Fornisce SOLO API asincrone "basate su pool". Tutto il codice node.js nel mondo, senza eccezioni, utilizza queste API asincrone basate su pool poiché sono semplicemente tutto ciò che è disponibile. Il filesystem ei processi figlio sono una storia diversa, ma il networking è costantemente asincrono.
Peter Lyons

4
Attento, Peter, per non essere la proverbiale pentola del suo bollitore. Vuole sapere come lo hanno fatto gli autori dell'API di rete, non come lo fanno le persone che usano l'API di rete. Alla fine ho capito come si comporta il nodo rispetto a: eventi non bloccanti perché volevo scrivere il mio codice non bloccante che non ha nulla a che fare con la rete o con qualsiasi altra API asincrona incorporata. È abbastanza chiaro che David vuole fare lo stesso.
Jason

2
Il nodo non utilizza i pool di thread per l'IO, utilizza l'IO nativo non bloccante, l'unica eccezione è fs, per quanto ne so
vkurchatkin

2

Thread pool come quando e chi ha usato:

Prima di tutto, quando usiamo / installiamo Node su un computer, avvia un processo tra gli altri processi che viene chiamato processo del nodo nel computer e continua a funzionare finché non lo uccidi. E questo processo in esecuzione è il nostro cosiddetto thread singolo.

inserisci qui la descrizione dell'immagine

Quindi il meccanismo del thread singolo rende facile bloccare un'applicazione del nodo ma questa è una delle caratteristiche uniche che Node.js porta in tavola. Quindi, ancora una volta, se esegui l'applicazione del nodo, verrà eseguita in un solo thread. Non importa se hai 1 o un milione di utenti che accedono alla tua applicazione contemporaneamente.

Quindi capiamo esattamente cosa succede nel singolo thread di nodejs quando avvii l'applicazione del nodo. All'inizio il programma viene inizializzato, quindi viene eseguito tutto il codice di primo livello, il che significa che tutti i codici che non sono all'interno di alcuna funzione di callback ( ricorda che tutti i codici all'interno di tutte le funzioni di callback verranno eseguiti in loop di eventi ).

Dopodiché, tutto il codice dei moduli eseguito quindi registra tutti i callback, infine, il ciclo di eventi è iniziato per la tua applicazione.

inserisci qui la descrizione dell'immagine

Quindi, come discusso in precedenza, tutte le funzioni di callback e i codici all'interno di tali funzioni verranno eseguiti in un ciclo di eventi. Nel loop degli eventi, i carichi sono distribuiti in diverse fasi. Ad ogni modo, non discuterò del loop di eventi qui.

Bene, per il sacco di una migliore comprensione del pool di thread, ti chiedo di immaginare che nel ciclo di eventi, i codici all'interno di una funzione di callback vengano eseguiti dopo aver completato l'esecuzione di codici all'interno di un'altra funzione di callback, ora se ci sono alcune attività sono effettivamente troppo pesanti. Quindi bloccherebbero il nostro thread singolo nodejs. E così, è qui che entra in gioco il pool di thread, che è proprio come il ciclo di eventi, fornito a Node.js dalla libreria libuv.

Quindi il pool di thread non fa parte di nodejs stesso, è fornito da libuv per scaricare compiti pesanti su libuv, e libuv eseguirà quei codici nei propri thread e dopo l'esecuzione libuv restituirà i risultati all'evento nel ciclo degli eventi.

inserisci qui la descrizione dell'immagine

Il pool di thread ci fornisce quattro thread aggiuntivi, questi sono completamente separati dal thread singolo principale. E possiamo effettivamente configurarlo fino a 128 thread.

Quindi tutti questi thread insieme hanno formato un pool di thread. e il ciclo di eventi può quindi scaricare automaticamente attività pesanti nel pool di thread.

La parte divertente è che tutto questo accade automaticamente dietro le quinte. Non siamo noi sviluppatori a decidere cosa va al pool di thread e cosa no.

Ci sono molte attività che vanno al pool di thread, come

-> All operations dealing with files
->Everyting is related to cryptography, like caching passwords.
->All compression stuff
->DNS lookups

0

Questo malinteso è semplicemente la differenza tra il multitasking preventivo e il multitasking cooperativo ...

Il sonno spegne l'intero carnevale perché c'è davvero una fila per tutte le giostre, e hai chiuso il cancello. Consideralo come "un interprete JS e alcune altre cose" e ignora i thread ... per te, c'è solo un thread, ...

... quindi non bloccarlo.

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.