Como o modelo IO não bloqueado de encadeamento único funciona no Node.js

Eu não sou um programador Node, mas estou interessado em como funciona o modelo IO não bloqueado de thread único . Depois que eu li o artigo compreendendo 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('Hello

Return from async DB query

'); c.end(); } );

Aqui vem uma pergunta. Quando há dois pedidos A (vem primeiro) e B, já que há apenas um único encadeamento, o programa do lado do servidor manipulará o pedido A em primeiro lugar: fazendo a consulta sql, que é uma instrução de suspensão para espera de E / S. E o programa está preso na espera de E / S e não pode executar o código que renderiza a página da Web. O programa mudará para o pedido B durante a espera? Na minha opinião, devido ao modelo de encadeamento único, não há como alternar um pedido de outro. Mas o título do código de exemplo diz que “tudo corre em paralelo, exceto o seu código”. (PS: Não tenho certeza se entendi mal o código ou não, já que nunca usei o Node.) Como o Node alterna de A para B durante a espera? E você pode explicar o modelo IO não bloqueado do Node de forma simples? Eu apreciaria se você pudesse me ajudar. 🙂

O Node.js é construído sobre o libuv , uma biblioteca multi-plataforma que abstrai as inputs / saídas para inputs / saídas assíncronas (sem bloqueio) fornecidas pelos SOs suportados (Unix, OS X e Windows, pelo menos).

IO asynchronous

Neste modelo de programação, a operação abrir / ler / escrever em dispositivos e resources (sockets, sistema de arquivos, etc.) gerenciados pelo sistema de arquivos não bloqueia o encadeamento de chamada (como no típico modelo síncrono c) e apenas marca o processo (na estrutura de dados do nível do kernel / SO) para ser notificado quando novos dados ou events estiverem disponíveis. No caso de um aplicativo do tipo servidor da web, o processo é responsável por descobrir a solicitação / contexto ao qual o evento notificado pertence e continuar processando a solicitação a partir daí. Observe que isso significará 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 ceder ao dispatcher de um processo para que um único processo encadeado manipule novos events.

O problema com o modelo que descrevi é que não é familiar e difícil de raciocinar para o programador, pois é de natureza não seqüencial. “Você precisa fazer um pedido na function A e lidar com o resultado em uma function diferente, onde os seus habitantes de A geralmente não estão disponíveis.”

Modelo do nó (estilo de passagem contínua e loop de events)

O nó aborda o problema de aproveitar os resources de linguagem do javascript para tornar esse modelo um pouco mais síncrono, induzindo o programador a empregar um determinado estilo de programação. Cada function que solicita IO tem uma assinatura como function (... parameters ..., callback) e precisa receber um callback que será invocado quando a operação solicitada for concluída (tenha em mente que a maior parte do tempo é gasto esperando para o sistema operacional para sinalizar a conclusão – o tempo que pode ser gasto fazendo outro trabalho). O suporte a Javascript para closures permite que você use variables ​​que você definiu na function externa (chamada) dentro do corpo do retorno de chamada – isso permite manter o estado entre diferentes funções que serão invocadas pelo tempo de execução do nó independentemente. Veja também Continuation Passing Style .

Além disso, após invocar uma function gerando uma operação de IO, a function de chamada geralmente return controle para o loop de events do nó . Esse loop chamará o próximo retorno de chamada ou function que foi planejada para execução (muito provavelmente porque o evento correspondente foi notificado pelo SO) – isso permite o processamento simultâneo de várias solicitações.

Você pode pensar no loop de events do nó como algo similar ao dispatcher do kernel: o kernel agendaria a execução de um thread bloqueado quando seu IO pendente for concluído enquanto o nó agendará um retorno de chamada quando o evento correspondente ocorrer.

Altamente concorrente, sem paralelismo

Como observação final, a frase “tudo roda em paralelo, exceto seu código” faz um trabalho decente de capturar o ponto em que o nó permite que seu código manipule solicitações de centenas de milhares de sockets abertos com um único encadeamento simultaneamente, multiplexando e sequenciando todos os seus js lógica em um único stream de execução (apesar de dizer que “tudo corre em paralelo” provavelmente não está correto aqui – veja concurrency vs Paralelismo – Qual é a diferença? ). Isso funciona muito bem para servidores webapp, já que a maior parte do tempo é gasto na espera por rede ou disco (database / sockets) e a lógica não exige muito da CPU, ou seja: isso funciona bem para cargas de trabalho vinculadas a E / S.

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

O Apache é um servidor HTTP multiencadeado, para cada e toda solicitação que o servidor recebe, ele cria um encadeamento separado que lida com essa solicitação.

O Node.js, por outro lado, é controlado por events, tratando todos os pedidos de forma assíncrona a partir de um único encadeamento.

Quando A e B são recebidos no apache, dois encadeamentos são criados para manipular solicitações. Cada um manipulando a consulta separadamente, cada um aguardando os resultados da consulta antes de veicular a página. A página só é exibida até que a consulta seja concluída. A busca de consulta está bloqueando porque o servidor não pode executar o restante do thread até receber o resultado.

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

Na minha opinião, como é um modelo de thread único, não há como alternar de um pedido para outro.

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

Editar

A consulta SQL é retirada da biblioteca mysql . Ele implementa o estilo de retorno de chamada e o emissor de evento 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. Quando o db está conectado, a consulta é passada para o servidor. As consultas podem ser enfileiradas.
  3. O loop de events principal é notificado da conclusão com retorno de chamada ou evento.
  4. O loop principal executa seu callback / eventhandler.

As solicitações recebidas para o servidor http são tratadas de maneira semelhante. A arquitetura de encadeamentos internos é algo assim:

loop de eventos node.js

Os encadeamentos C ++ são os libuv que fazem a E / S assíncrona (disco ou rede). O loop de events principal continua a executar após o envio da solicitação para o conjunto de encadeamentos. Pode aceitar mais pedidos, pois não espera nem dorme. Consultas SQL / solicitações HTTP / sistema de arquivos lê tudo isso dessa maneira.

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

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

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

  • espera do timer está completa
  • o próximo bloco de dados está pronto para ser gravado neste arquivo
  • há uma nova solicitação HTTP vindo em nossa direção

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

(Há um pouco de magia sob o capô, onde os events se originam. Alguns envolvem threads de trabalho de baixo nível sendo executados em paralelo.)

Neste caso de SQL, há muitas coisas (events) acontecendo entre fazer a consulta de database e obter seus resultados no retorno de chamada . Durante esse tempo, o loop de events continua bombeando a vida para o aplicativo e avançando outros pedidos, um pequeno evento de cada vez. Portanto, várias solicitações estão sendo atendidas simultaneamente.

visão de alto nível de loop de eventos

De acordo com: “Loop de events de 10.000 pés – conceito básico por trás do Node.js” .

A function c.query () tem dois argumentos

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

A operação “Fetch Data”, neste caso, é um DB-Query, agora isso pode ser manipulado pelo Node.js, gerando um thread de trabalho e dando a ele a tarefa de executar o DB-Query. (Lembre-se de que o Node.js pode criar encadeamento internamente). Isso permite que a function retorne instantaneamente sem qualquer atraso

O segundo argumento “Pós-processamento de dados” é uma function de retorno de chamada, a estrutura do nó registra esse retorno de chamada e é chamada pelo loop de events.

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

PS: Acabei de começar a entender o node, na verdade eu queria escrever isso como comentário para o @Philip, mas como não tinha pontos de reputação suficientes, escrevi como uma resposta.

se você ler um pouco mais – “É claro que, no backend, existem threads e processos para access ao database 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 que as interações de E / S, por exemplo, com o database ou com outros processos, serão assíncronas da perspectiva de cada solicitação, pois os resultados desses encadeamentos são retornados por meio do loop de events para o seu código. ”

about – “tudo é executado 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 esperar por IO, o evento loop trata tudo e invoca o retorno de chamada. Não é algo que você tenha que pensar.

no seu exemplo: há duas requisições A (vem primeiro) e B. você executa a requisição A, seu código continua sendo executado de forma síncrona e executa a requisição B. o evento loop trata a requisição A, quando termina invoca o retorno de chamada da requisição A com o resultado, mesmo vai para pedir B.

Ok, a maioria das coisas deve ficar clara até agora … a parte complicada é o SQL : se não estiver na realidade em execução em outro thread ou processo em sua totalidade, a execução de SQL deve 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, o sono) podem ser transferidos para o kernel (como uma interrupção / evento de alarme) e colocados na lista de events para o loop principal.

Isso significa que, por exemplo, a interpretação do SQL, etc. é feita imediatamente, mas durante a espera (armazenada como um evento para vir no futuro pelo kernel em alguma estrutura kqueue, epoll, …; juntamente com as outras operações de IO) ) o loop principal pode fazer outras coisas e, eventualmente, verificar se algo aconteceu àqueles IOs e espera.

Então, para reformulá-lo novamente: o programa nunca é (permitido ficar) preso, as chamadas para dormir nunca são executadas. Seu dever é feito pelo kernel (escreva algo, espere que algo apareça na rede, espere o tempo passar) ou outro segmento ou processo. – O processo Node verifica se pelo menos uma dessas tarefas é finalizada pelo kernel na única chamada de bloqueio para o sistema operacional uma vez em cada ciclo de ciclo de events. Esse ponto é alcançado quando tudo o que não bloqueia é feito.

Claro? 🙂

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