Quando o pool de threads é usado?

104

Portanto, tenho uma compreensão de como o Node.js funciona: ele tem um único thread de ouvinte que recebe um evento e o delega a um pool de trabalho. O thread de trabalho notifica o ouvinte assim que conclui o trabalho e, então, o ouvinte retorna a resposta ao chamador.

Minha pergunta é a seguinte: se eu abrir um servidor HTTP em Node.js e chamar o sleep em um dos meus eventos de caminho roteado (como "/ test / sleep"), todo o sistema irá parar. Até mesmo o segmento de ouvinte único. Mas meu entendimento é que esse código está acontecendo no pool de trabalhadores.

Agora, por outro lado, quando eu uso o Mongoose para conversar com o MongoDB, as leituras do banco de dados são uma operação de E / S cara. O Node parece ser capaz de delegar o trabalho a um encadeamento e receber o retorno de chamada quando ele for concluído; o tempo que leva para carregar do banco de dados não parece bloquear o sistema.

Como o Node.js decide usar um thread do pool de threads em vez do thread do listener? Por que não consigo escrever código de evento que dorme e apenas bloqueia um thread do pool de threads?

Haney
fonte
@Tobi - Já vi isso. Ainda não responde à minha pergunta. Se o trabalho estivesse em outro encadeamento, o sono afetaria apenas esse encadeamento e não o ouvinte também.
Haney
8
Uma pergunta genuína, em que você tenta entender algo sozinho e, quando não consegue encontrar uma saída para o labirinto, pede ajuda.
Rafael Eyng de

Respostas:

240

Seu entendimento de como o nó funciona não está correto ... mas é um equívoco comum, porque a realidade da situação é na verdade bastante complexa e normalmente resumida em pequenas frases incisivas como "o nó é de thread único" que simplificam as coisas .

Por enquanto, ignoraremos o multiprocessamento / multi-threading explícito por meio de cluster e webworker-threads , e apenas falaremos sobre o nó não-threaded típico.

O Node é executado em um único loop de evento. É um único tópico e você só consegue aquele único tópico. Todo o javascript que você escreve é ​​executado neste loop e, se uma operação de bloqueio acontecer nesse código, bloqueará todo o loop e nada mais acontecerá até que termine. Esta é a natureza tipicamente de um único nó sobre o qual você tanto ouve falar. Mas, não é toda a imagem.

Determinadas funções e módulos, geralmente escritos em C / C ++, suportam E / S assíncronas. Quando você chama essas funções e métodos, eles gerenciam internamente a passagem da chamada para um thread de trabalho. Por exemplo, quando você usa o fsmódulo para solicitar um arquivo, o fsmódulo passa essa chamada para um thread de trabalho, e esse trabalhador espera por sua resposta, que então apresenta de volta para o loop de evento que está ocorrendo sem ele no entretanto. Tudo isso é abstraído de você, o desenvolvedor do nó, e parte dele é abstraído dos desenvolvedores do módulo através do uso do libuv .

Conforme apontado por Denis Dollfus nos comentários ( desta resposta a uma pergunta semelhante), a estratégia usada por libuv para alcançar E / S assíncronas nem sempre é um pool de threads, especificamente no caso do httpmódulo, uma estratégia diferente parece ser usado neste momento. Para nossos propósitos aqui, é principalmente importante observar como o contexto assíncrono é alcançado (usando libuv) e que o pool de threads mantido por libuv é uma das várias estratégias oferecidas por essa biblioteca para atingir a assincronicidade.


Em uma tangente principalmente relacionada, há uma análise muito mais profunda de como o nó atinge a assincronicidade e alguns problemas potenciais relacionados e como lidar com eles, neste excelente artigo . A maior parte expande o que escrevi acima, mas, adicionalmente, aponta:

  • Qualquer módulo externo que você incluir em seu projeto que faz uso de C ++ nativo e libuv provavelmente usará o pool de threads (pense: acesso ao banco de dados)
  • libuv tem um tamanho de pool de threads padrão de 4, e usa uma fila para gerenciar o acesso ao pool de threads - o resultado é que se você tiver 5 consultas de banco de dados de longa duração, todas ao mesmo tempo, uma delas (e qualquer outra assíncrona ação que depende do pool de threads) estará esperando que essas consultas terminem antes mesmo de começar
  • Você pode atenuar isso aumentando o tamanho do pool de threads por UV_THREADPOOL_SIZEmeio da variável de ambiente, desde que faça isso antes que o pool de threads seja necessário e criado:process.env.UV_THREADPOOL_SIZE = 10;

Se você quiser o multiprocessamento tradicional ou multi-threading no nó, você pode obtê-lo através do clustermódulo embutido ou vários outros módulos, como o mencionado acima webworker-threads, ou você pode fingir implementando alguma forma de fragmentar seu trabalho e manualmente usando setTimeoutou setImmediateou process.nextTickpara pausar seu trabalho e continuá-lo em um loop posterior para permitir que outros processos sejam concluídos (mas isso não é recomendado).

Observe que, se você está escrevendo um código de longa execução / bloqueio em javascript, provavelmente está cometendo um erro. Outros idiomas terão um desempenho muito mais eficiente.

Jason
fonte
1
Puta merda, isso esclarece tudo para mim. Muito obrigado @Jason!
Haney
5
Não tem problema :) Eu me encontrei onde você está não muito tempo atrás, e foi difícil chegar a uma resposta bem definida porque de um lado você tem desenvolvedores C / C ++ para os quais a resposta é óbvia, e do outro você tem o típico desenvolvedores da web que não se aprofundaram muito nesse tipo de questão antes. Eu nem tenho certeza se minha resposta está 100% tecnicamente correta quando você chega ao nível C, mas está certa em termos gerais.
Jason,
3
Usar o pool de threads para solicitações de rede seria um grande desperdício de recursos. De acordo com esta pergunta "Ele faz a E / S de rede assíncrona com base nas interfaces de E / S assíncronas em diferentes plataformas, como epoll, kqueue e IOCP, sem um pool de threads" - o que faz sentido.
Denis Dollfus
1
... dito isso, se você fizer algum trabalho pesado no thread principal de javascript diretamente, ou não tiver recursos suficientes ou não gerenciá-los de forma adequada para dar espaço suficiente para o threadpool, você pode introduzir lag em uma concorrência mais baixa limite - o resultado é que, para os mesmos recursos do sistema, você normalmente experimentará mais thruput com node.js do que com outras opções (embora existam outros sistemas baseados em eventos em outras linguagens que visam desafiar isso - eu não visto benchmarks recentes embora) - é claro que um modelo baseado em evento supera um modelo encadeado.
Jason
1
@Aabid O encadeamento do ouvinte não executa uma consulta ao banco de dados, então levará cerca de 6 segundos para que todas as 10 dessas consultas sejam concluídas (pelo tamanho do pool de encadeamentos padrão de 4). Se você precisar fazer qualquer trabalho em javascript que não exija que os resultados dessa consulta de banco de dados sejam concluídos, por exemplo, mais solicitações chegam que não exigem que nenhum trabalho assíncrono seja concluído pelo pool de threads, ele continuará a funcionar no principal loop de eventos.
Jason
20

Portanto, tenho uma compreensão de como o Node.js funciona: ele tem um único thread de ouvinte que recebe um evento e o delega a um pool de trabalho. O thread de trabalho notifica o ouvinte assim que conclui o trabalho e, então, o ouvinte retorna a resposta ao chamador.

Isso não é realmente preciso. O Node.js tem apenas um único thread de "trabalho" que executa a execução de javascript. Existem threads dentro do nó que tratam do processamento IO, mas pensar neles como "trabalhadores" é um equívoco. Na verdade, existem apenas manipulação de IO e alguns outros detalhes da implementação interna do nó, mas, como um programador, você não pode influenciar seu comportamento a não ser alguns parâmetros diversos, como MAX_LISTENERS.

Minha pergunta é a seguinte: se eu abrir um servidor HTTP em Node.js e chamar o sleep em um dos meus eventos de caminho roteado (como "/ test / sleep"), todo o sistema irá parar. Até mesmo o segmento de ouvinte único. Mas meu entendimento é que esse código está acontecendo no pool de trabalhadores.

Não há mecanismo de suspensão em JavaScript. Poderíamos discutir isso mais concretamente se você postasse um trecho de código do que você acha que significa "dormir". Não existe essa função a ser chamada para simular algo como time.sleep(30)em python, por exemplo. Há setTimeout, mas que é fundamentalmente não dorme. setTimeoute liberesetInterval explicitamente , não bloqueie, o loop de eventos para que outros bits de código possam ser executados no thread de execução principal. A única coisa que você pode fazer é um loop ocupado da CPU com computação na memória, o que de fato deixará o thread de execução principal sem resposta e deixará o programa sem resposta.

Como o Node.js decide usar um thread do pool de threads em vez do thread do listener? Por que não consigo escrever código de evento que dorme e apenas bloqueia um thread do pool de threads?

O IO da rede é sempre assíncrono. Fim da história. O Disk IO tem APIs síncronas e assíncronas, portanto, não há "decisão". node.js se comportará de acordo com as funções principais da API que você chama de sincronização vs assíncrona normal. Por exemplo: fs.readFilevs fs.readFileSync. Para processos filho, também existem APIs child_process.exece separadas child_process.execSync.

A regra é sempre usar APIs assíncronas. Os motivos válidos para usar as APIs de sincronização são para o código de inicialização em um serviço de rede antes de escutar conexões ou em scripts simples que não aceitam solicitações de rede para ferramentas de construção e esse tipo de coisa.

Peter Lyons
fonte
1
De onde vêm essas APIs assíncronas? Eu entendo o que você está dizendo, mas quem escreveu essas APIs optou por IOCP / async. Como eles escolheram fazer isso?
Haney
3
Sua pergunta é como ele escreveria seu próprio código intensivo de tempo e não bloquearia.
Jason,
1
Sim. O Node fornece rede UDP, TCP e HTTP básica. Ele fornece APENAS APIs "baseadas em pool" assíncronas. Todo o código node.js do mundo, sem exceção, usa essas APIs assíncronas baseadas em pool, pois há simplesmente tudo o que está disponível. O sistema de arquivos e os processos filho são uma história diferente, mas a rede é consistentemente assíncrona.
Peter Lyons,
4
Cuidado, Peter, para que você não seja o proverbial caldeirão dele. Ele quer saber como os escritores da API de rede fizeram isso, não como as pessoas que usam a API de rede fazem isso. Eventualmente, ganhei uma compreensão de como o node se comporta em relação a eventos não bloqueadores porque eu queria escrever meu próprio código não bloqueador que não tivesse nada a ver com rede ou qualquer uma das outras APIs assíncronas embutidas. É bastante claro que David deseja fazer o mesmo.
Jason,
2
O Node não usa pools de threads para IO, ele usa IO sem bloqueio nativo, a única exceção é fs, até onde eu sei
vkurchatkin
2

Pool de threads como quando e quem usou:

Em primeiro lugar, quando usamos / instalamos o Node em um computador, ele inicia um processo entre outros processos que é chamado de processo de nó no computador, e continua em execução até que você o mate. E este processo em execução é nosso chamado single thread.

insira a descrição da imagem aqui

Portanto, o mecanismo de thread único facilita o bloqueio de um aplicativo de nó, mas esse é um dos recursos exclusivos que o Node.js traz para a mesa. Portanto, novamente se você executar seu aplicativo de nó, ele será executado em apenas um único thread. Não importa se você tem 1 ou milhão de usuários acessando seu aplicativo ao mesmo tempo.

Portanto, vamos entender exatamente o que acontece no único thread de nodejs quando você inicia seu aplicativo de nó. Inicialmente o programa é inicializado, então todo o código de nível superior é executado, o que significa que todos os códigos que não estão dentro de nenhuma função de retorno de chamada ( lembre-se de que todos os códigos dentro de todas as funções de retorno de chamada serão executados no loop de evento ).

Depois disso, todos os códigos dos módulos executados registram todo o retorno de chamada, por fim, o loop de eventos iniciado para sua aplicação.

insira a descrição da imagem aqui

Como discutimos antes, todas as funções de retorno de chamada e códigos dentro dessas funções serão executados no loop de eventos. No loop de eventos, as cargas são distribuídas em diferentes fases. De qualquer forma, não vou discutir sobre loop de eventos aqui.

Bem, para uma melhor compreensão do pool de Threads, estou pedindo que você imagine que, no loop de eventos, os códigos dentro de uma função de retorno de chamada são executados após a conclusão da execução de códigos dentro de outra função de retorno de chamada, agora se houver algumas tarefas, são realmente muito pesadas. Eles então bloqueariam nosso thread único nodejs. E é aí que entra o pool de threads, que é exatamente como o loop de evento, fornecido ao Node.js pela biblioteca libuv.

Portanto, o pool de threads não faz parte do nodejs em si, é fornecido pelo libuv para descarregar tarefas pesadas para o libuv, e o libuv executará esses códigos em suas próprias threads e, após a execução, o libuv retornará os resultados para o evento no loop de eventos.

insira a descrição da imagem aqui

O pool de threads nos dá quatro threads adicionais, que são completamente separados do thread único principal. E podemos realmente configurá-lo para até 128 threads.

Portanto, todos esses threads juntos formaram um pool de threads. e o loop de eventos pode então descarregar automaticamente tarefas pesadas para o pool de threads.

A parte divertida é que tudo isso acontece automaticamente nos bastidores. Não somos nós, desenvolvedores, que decidimos o que vai e o que não vai para o pool de threads.

Existem muitas tarefas que vão para o pool de threads, como

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

Este mal-entendido é apenas a diferença entre multitarefa preventiva e multitarefa cooperativa ...

O sono desliga todo o carnaval porque há realmente uma fila para todas as atrações e você fechou o portão. Pense nisso como "um interpretador JS e algumas outras coisas" e ignore os threads ... para você, há apenas um thread, ...

... então não bloqueie.

Gregory R. Sudderth
fonte