Como o modelo de E / S sem bloqueio de thread único funciona no Node.js


325

Eu não sou um programador de Nó, mas estou interessado em como o modelo de E / S sem bloqueio de thread único funciona. Depois de ler o artigo entendendo-o-nó-js-evento-loop , estou realmente confuso sobre isso. Deu um exemplo para o modelo:

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 há duas solicitações A (vem primeiro) e B, uma vez que existe apenas um encadeamento, o programa do lado do servidor manipula a solicitação A primeiro: fazer consultas SQL é uma instrução adormecida que espera por E / S. E o programa está bloqueado na I/Oespera e não pode executar o código que renderiza a página da web. O programa mudará para solicitar B durante a espera? Na minha opinião, por causa do modelo de thread único, não há como alternar uma solicitação de outra. Mas o título do código de exemplo diz que tudo é executado em paralelo, exceto o seu código .

(PS: Não sei se entendi errado o código ou não, pois nunca usei o Nó.) Como o Nó alterna A para B durante a espera? E você pode explicar o modelo de E / S não bloqueante de um único modo de maneira simples? Eu apreciaria se você pudesse me ajudar. :)

Respostas:


374

O Node.js é construído sobre o libuv , uma biblioteca de plataforma cruzada que abstrai apis / syscalls para entrada / saída assíncrona (sem bloqueio) fornecida pelos sistemas operacionais suportados (Unix, OS X e Windows, pelo menos).

E / S assíncronas

Nesse modelo de programação, a operação de abertura / leitura / gravação em dispositivos e recursos (soquetes, sistema de arquivos etc.) gerenciados pelo sistema de arquivos não bloqueia o encadeamento de chamada (como no modelo c-type síncrono típico) e apenas marque o processo (na estrutura de dados no nível do kernel / SO) a ser notificado quando novos dados ou eventos estiverem disponíveis. No caso de um aplicativo semelhante a um servidor da Web, o processo é responsável por descobrir a qual solicitação / contexto o evento notificado pertence e continuar processando a solicitação a partir daí. Observe que isso significa necessariamente que você estará em um quadro de pilha diferente daquele que originou a solicitação para o sistema operacional, pois o último teve que render ao despachante de um processo para que um único processo encadeado manipulasse novos eventos.

O problema com o modelo que descrevi é que não é familiar e difícil de raciocinar para o programador, pois é de natureza não sequencial. "Você precisa fazer uma solicitação na função A e manipular o resultado em uma função diferente, onde os locais de A geralmente não estão disponíveis."

Modelo do nó (estilo de passagem de continuação e loop de eventos)

O Node resolve o problema de alavancar os recursos de linguagem do javascript para tornar esse modelo um pouco mais síncrono, induzindo o programador a empregar um certo estilo de programação. Toda função que solicita E / S possui uma assinatura semelhante function (... parameters ..., callback)e precisa receber um retorno de chamada que será chamado quando a operação solicitada for concluída (lembre-se de que a maior parte do tempo é gasta esperando o sistema operacional sinalizar a conclusão - tempo que pode ser gasto fazendo outro trabalho). O suporte do Javascript para encerramentos permite que você use variáveis ​​definidas na função externa (chamada) dentro do corpo do retorno de chamada - isso permite manter o estado entre as diferentes funções que serão chamadas pelo tempo de execução do nó independentemente. Consulte também Estilo de passagem de continuação .

Além disso, após chamar uma função que gera uma operação de E / S, a função de chamada geralmente returncontrola o loop de eventos do nó . Esse loop invocará o próximo retorno de chamada ou função que foi agendada para execução (provavelmente porque o evento correspondente foi notificado pelo sistema operacional) - isso permite o processamento simultâneo de várias solicitações.

Você pode pensar no loop de eventos do nó como algo semelhante ao despachante do kernel: o kernel planejaria a execução de um encadeamento bloqueado assim que sua IO pendente fosse concluída, enquanto o nó agendaria um retorno de chamada quando o evento correspondente ocorresse.

Altamente simultâneo, sem paralelismo

Como observação final, a frase "tudo roda em paralelo, exceto o seu código" faz um trabalho decente de capturar o ponto em que o nó permite que seu código lide com solicitações de centenas de milhares de soquetes abertos com um único encadeamento simultaneamente, multiplexando e sequenciando todos os seus js lógica em um único fluxo de execução (mesmo que dizer "tudo corra em paralelo" provavelmente não esteja correto aqui - consulte Concorrência versus paralelismo - qual é a diferença? ). Isso funciona muito bem para servidores de aplicativos da Web, pois na maioria das vezes é gasto na espera de rede ou disco (banco de dados / soquetes) e a lógica não exige muita CPU - ou seja: isso funciona bem para cargas de trabalho ligadas a IO .


45
Perguntas de acompanhamento: como a E / S realmente acontece? O nó está fazendo uma solicitação ao sistema e solicitando ser notificado quando terminar. Então, o sistema está executando um encadeamento que está executando a E / S ou o sistema também está executando a E / S de forma assíncrona no nível do hardware usando interrupções? Algo em algum lugar tem que esperar a E / S terminar, e isso bloqueará até que seja feito e consuma uma certa quantidade de recursos.
Philip

6
Acabei de perceber que este comentário de acompanhamento é respondido por @ user568109 abaixo. Desejo que haja uma maneira de mesclar essas duas respostas.
Lfalin

4
Eu gostaria que você escrevesse uma resposta duas vezes mais, para que eu entendesse duas vezes melhor.
Rafael Eyng

O nó é suportado em muitos lugares, para o registro. Quando eu estava projetando o firmware para os roteadores MIPS32, o Node.JS podia ser executado naqueles via OpenWRT.
Qix - MONICA FOI ERRADA EM

Como pontua sobre o apache? O Apache também é capaz de lidar com conexões simultâneas com um encadeamento separado.
Suhail Gupta

210

Bem, para dar uma perspectiva, deixe-me comparar o node.js com o apache.

O Apache é um servidor HTTP multithread, para cada solicitação que o servidor recebe, cria uma thread separada que lida com essa solicitação.

Por outro lado, o Node.js é orientado a eventos, manipulando todos os pedidos de forma assíncrona a partir de um único encadeamento.

Quando A e B são recebidos no apache, dois threads são criados para lidar com solicitações. Cada um manipulando a consulta separadamente, cada um aguardando os resultados da consulta antes de exibir a página. A página é exibida apenas até a consulta ser concluída. A busca de consulta está bloqueando porque o servidor não pode executar o restante do encadeamento até receber o resultado.

No nó, c.query é tratado de forma assíncrona, o que significa que enquanto c.query busca os resultados para A, ele salta para manipular c.query para B e, quando os resultados chegam para A chegam, os resultados são retornados ao retorno de chamada que envia o resposta. O Node.js sabe executar retorno de chamada quando a busca é concluída.

Na minha opinião, por ser um modelo de thread único, não há como alternar de uma solicitação para outra.

Na verdade, o servidor do nó faz exatamente isso para você o tempo todo. Para fazer opções, (o comportamento assíncrono) a maioria das funções que você usaria terá retornos de chamada.

Editar

A consulta SQL é obtida da biblioteca mysql . Ele implementa o estilo de retorno de chamada e o emissor de eventos para enfileirar solicitações SQL. Ele não os executa de forma assíncrona, isso é feito pelos encadeamentos libuv internos que fornecem a abstração de E / S sem bloqueio. As etapas a seguir acontecem para fazer uma consulta:

  1. Abra uma conexão com o db, a conexão em si pode ser feita de forma assíncrona.
  2. Depois que o db é conectado, a consulta é passada para o servidor. As consultas podem ser enfileiradas.
  3. O loop do evento principal é notificado da conclusão com retorno de chamada ou evento.
  4. O loop principal executa seu retorno de chamada / manipulador de eventos.

As solicitações recebidas para o servidor http são tratadas da mesma maneira. A arquitetura interna do thread é mais ou menos assim:

loop de eventos node.js

Os encadeamentos C ++ são os libuv que executam a E / S assíncrona (disco ou rede). O loop do evento principal continua a ser executado após o envio da solicitação ao conjunto de encadeamentos. Ele pode aceitar mais solicitações, pois não espera ou dorme. As consultas SQL / solicitações HTTP / sistema de arquivos leem tudo dessa maneira.


16
O diagrama é muito útil.
Anmol Saraf

14
Espere, então no seu diagrama você tem o "pool de threads C ++ interno", o que significa que todas as operações de bloqueio de E / S gerarão um thread, certo? Portanto, se meu aplicativo Node funcionar com alguma E / S para todas as solicitações , não há praticamente nenhuma diferença entre o modelo Node e o Apache? Não estou arrependendo esta parte.
precisa saber é o seguinte

21
@ gav.newalkar Eles não geram um thread, os pedidos estão na fila. Os encadeamentos no conjunto de encadeamentos os processam. Os encadeamentos não são dinâmicos e por solicitação, como no Apache. Eles geralmente são fixos e diferem de sistema para sistema.
user568109

10
@ user568109 Mas o Apache também está usando um pool de threads ( httpd.apache.org/docs/2.4/mod/worker.html ). Portanto, no final, a diferença entre uma configuração com o node.js. difere daquela com o Apache na frente apenas no local onde o pool de threads está localizado, não é?
Kris

13
Esse diagrama deve estar na primeira página dos documentos oficiais.
precisa saber é o seguinte

52

O Node.js usa o libuv nos bastidores. O libuv possui um conjunto de encadeamentos (do tamanho 4, por padrão). Portanto, o Node.js usa threads para obter simultaneidade.

No entanto , seu código é executado em um único thread (ou seja, todos os retornos de chamada das funções do Node.js. serão chamados no mesmo thread, o chamado loop-thread ou event-loop). Quando as pessoas dizem "Node.js é executado em um único encadeamento", estão realmente dizendo "os retornos de chamada do Node.js são executados em um único encadeamento".


1
Resposta curta, mas clara (y)
Sudhanshu Gaur

1
boa resposta que eu gostaria de acrescentar que I / O acontece fora deste evento de circuito principal, loop-fio, pedido-thread
Ionut Popa

essa é a resposta que eu estava procurando a partir de 2 horas, como a simultaneidade foi gerenciados no aplicativo de rosca única
Muhammad Ramzan

Sim, é difícil obter a resposta do 'próximo nível'. Isso explica onde o IO realmente é feito (em um pool de threads em outro lugar)
Oliver Shaw

9

O Node.js é baseado no modelo de programação do loop de eventos. O loop de eventos é executado em um único thread e aguarda repetidamente por eventos e, em seguida, executa quaisquer manipuladores de eventos inscritos nesses eventos. Eventos podem ser por exemplo

  • a espera do temporizador está concluída
  • o próximo pedaço de dados está pronto para ser gravado neste arquivo
  • há uma nova solicitação HTTP nova chegando em nosso caminho

Tudo isso é executado em thread único e nenhum código JavaScript é executado em paralelo. Desde que esses manipuladores de eventos sejam pequenos e esperem por mais eventos, tudo funcionará bem. Isso permite que várias solicitações sejam tratadas simultaneamente por um único processo Node.js.

(Há um pouco de mágica sob o capô como a origem dos eventos. Alguns deles envolvem threads de trabalho de baixo nível que são executados em paralelo.)

Nesse caso SQL, muitas coisas (eventos) acontecem entre fazer a consulta ao banco de dados e obter seus resultados no retorno de chamada . Durante esse período, o loop de eventos continua aumentando a vida útil do aplicativo e avançando em outras solicitações, um pequeno evento de cada vez. Portanto, várias solicitações estão sendo atendidas simultaneamente.

visualização de alto nível do loop de eventos

De acordo com: "Loop de eventos do conceito de 10.000 pés - núcleo por trás do Node.js." .


5

A função c.query () possui dois argumentos

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

A operação "Buscar Dados", neste caso, é uma Consulta ao Banco de Dados, agora isso pode ser tratado pelo Node.js gerando um encadeamento de trabalho e fornecendo a tarefa de executar a Consulta ao Banco de Dados. (Lembre-se de que o Node.js pode criar encadeamentos internamente). Isso permite que a função retorne instantaneamente sem demora

O segundo argumento "Pós-processamento de dados" é uma função de retorno de chamada, a estrutura do nó registra esse retorno de chamada e é chamada pelo loop de eventos.

Assim, a instrução c.query (paramenter1, parameter2)retornará instantaneamente, permitindo que o nó atenda a outra solicitação.

PS: Eu apenas comecei a entender o nó, na verdade, eu queria escrever isso como comentário para @Philip, mas como não tinha pontos de reputação suficientes, escrevi como resposta.


3

se você ler um pouco mais - "Claro, no back-end, existem threads e processos para acesso ao banco de dados e execução de processos. No entanto, eles não são explicitamente expostos ao seu código, portanto você não pode se preocupar com eles além de saber que as interações de E / S, por exemplo, com o banco de dados ou com outros processos serão assíncronas da perspectiva de cada solicitação, uma vez que os resultados desses encadeamentos são retornados pelo loop de eventos ao seu código ".

about - "tudo roda em paralelo, exceto o seu código" - seu código é executado de forma síncrona, sempre que você invoca uma operação assíncrona, como aguardar IO, o loop de eventos lida com tudo e chama o retorno de chamada. simplesmente não é algo que você tem que pensar.

no seu exemplo: existem duas solicitações A (vem primeiro) e B. você executa a solicitação A, seu código continua sendo executado de forma síncrona e executa a solicitação B. o loop de eventos manipula a solicitação A; quando termina, invoca o retorno de chamada da solicitação A com o resultado, o mesmo vale para a solicitação B.


3
"É claro que, no back-end, existem threads e processos para acesso ao banco de dados e execução do processo. No entanto, eles não são explicitamente expostos ao seu código" - Se eu usar essa frase, não vejo diferença entre o nó do ou qualquer estrutura multithread - digamos, o Spring Framework do Java - sim. Existem threads, mas você não controla a criação deles.
Rafael Eyng

@RafaelEyng Eu acho que, para lidar com a série de solicitações múltiplas, o nó sempre terá um único thread para isso. Não tenho certeza se cada retorno de chamada é colocado em uma nova instância de threads, além de outros processos, como acesso db, mas pelo menos sabemos que o nó não instancia os threads toda vez que recebe uma solicitação que terá que esperar na fila antes do processamento (execuções antes retorno de chamada).
Cold Cerberus

1

Ok, a maioria das coisas deve estar clara até agora ... a parte complicada é o SQL : se na verdade não estiver sendo executado em outro segmento ou processo na sua totalidade, a execução do SQL deverá ser dividida em etapas individuais (por um Processador SQL feito para execução assíncrona!), Onde os não bloqueadores são executados e os bloqueadores (por exemplo, a suspensão) podem ser transferidos para o kernel (como uma interrupção / evento de alarme) e colocados na lista de eventos para o laço principal.

Isso significa que, por exemplo, a interpretação do SQL, etc., é feita imediatamente, mas durante a espera (armazenada como um evento que o kernel vem no futuro em alguma estrutura do kqueue, epoll, ...; junto com outras operações de IO ) o loop principal pode fazer outras coisas e, eventualmente, verificar se algo aconteceu com esses pedidos de veiculação e aguarda.

Então, para reformulá-lo novamente: o programa nunca fica preso, as chamadas em suspensão nunca são executadas. O dever deles é cumprido pelo kernel (escreva alguma coisa, espere que algo apareça na rede, aguarde o tempo decorrido) ou outro thread ou processo. - O processo Nó verifica se pelo menos uma dessas tarefas foi concluída pelo kernel na única chamada de bloqueio para o SO uma vez em cada ciclo de loop de eventos. Esse ponto é alcançado quando tudo o que não é bloqueado é feito.

Claro? :-)

Eu não conheço Node. Mas de onde vem o c.query?


O epque do kqueue é para notificação de E / S assíncrona escalável no kernel do linux. Node tem libuv para isso. O nó está inteiramente na terra do usuário. Não depende do que o kernel implementa.
user568109

1
@ user568109, libuv é o intermediário de Node. Qualquer estrutura assíncrona depende (diretamente ou não) de algum suporte de E / S assíncrona no kernel. Assim?
22413 Robert Siemer #

Desculpe pela confusão. As operações de soquete requerem E / S sem bloqueio do kernel. Ele cuida do manuseio assíncrono. Mas a E / S de arquivo assíncrona é tratada pelo próprio libuv. Sua resposta não diz isso. Ele trata os dois da mesma forma, sendo manipulados pelo kernel.
user568109
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.