Se você precisar fazer essa pergunta, provavelmente não está familiarizado com o que a maioria dos aplicativos / serviços da Web faz. Você provavelmente está pensando que todos os softwares fazem isso:
user do an action
│
v
application start processing action
└──> loop ...
└──> busy processing
end loop
└──> send result to user
No entanto, não é assim que aplicativos da Web, ou qualquer aplicativo com um banco de dados como back-end, funcionam. Os aplicativos da Web fazem isso:
user do an action
│
v
application start processing action
└──> make database request
└──> do nothing until request completes
request complete
└──> send result to user
Nesse cenário, o software gasta a maior parte de seu tempo de execução usando 0% de tempo de CPU aguardando o retorno do banco de dados.
Aplicativo de rede multithread:
Os aplicativos de rede multithread lidam com a carga de trabalho acima da seguinte maneira:
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
Portanto, o encadeamento passa a maior parte do tempo usando 0% da CPU aguardando o retorno do banco de dados. Ao fazer isso, eles tiveram que alocar a memória necessária para um encadeamento que inclui uma pilha de programas completamente separada para cada encadeamento etc. Além disso, teriam que iniciar um encadeamento que, embora não seja tão caro quanto iniciar um processo completo, ainda não é exatamente barato.
Loop de evento com leitura única
Como passamos a maior parte do tempo usando 0% da CPU, por que não executar algum código quando não estamos usando a CPU? Dessa forma, cada solicitação ainda terá o mesmo tempo de CPU que os aplicativos multithread, mas não precisamos iniciar um encadeamento. Então fazemos isso:
request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response
Na prática, ambas as abordagens retornam dados com aproximadamente a mesma latência, já que é o tempo de resposta do banco de dados que domina o processamento.
A principal vantagem aqui é que não precisamos gerar um novo encadeamento, portanto não precisamos fazer muito e muito malloc, o que nos atrasaria.
Rosca mágica, invisível
A coisa aparentemente misteriosa é como as duas abordagens acima conseguem executar a carga de trabalho em "paralelo"? A resposta é que o banco de dados é encadeado. Portanto, nosso aplicativo de thread único está realmente aproveitando o comportamento multithread de outro processo: o banco de dados.
Onde a abordagem de falha única falha
Um aplicativo de leitura única falha muito se você precisar fazer muitos cálculos de CPU antes de retornar os dados. Agora, não quero dizer um loop for processando o resultado do banco de dados. Ainda é principalmente O (n). O que quero dizer é fazer transformações de Fourier (codificação mp3, por exemplo), traçado de raios (renderização em 3D) etc.
Outra armadilha dos aplicativos de leitura única é que ele utilizará apenas um único núcleo da CPU. Portanto, se você possui um servidor quad-core (hoje em dia não é incomum), não está usando os outros três núcleos.
Onde a abordagem multithread falha
Um aplicativo multithread falha muito se você precisar alocar muita RAM por thread. Primeiro, o uso da RAM significa que você não pode lidar com tantas solicitações quanto um aplicativo com um único filtro. Pior, malloc é lento. Alocar muitos e muitos objetos (o que é comum em estruturas modernas da Web) significa que podemos acabar sendo mais lentos do que aplicativos com um único filtro. É aqui que o node.js geralmente vence.
Um caso de uso que acaba piorando os multithread é quando você precisa executar outra linguagem de script em seu encadeamento. Primeiro, você geralmente precisa centralizar todo o tempo de execução para esse idioma, depois precisa centralizar as variáveis usadas pelo seu script.
Portanto, se você estiver escrevendo aplicativos de rede em C ou em go ou java, a sobrecarga do encadeamento geralmente não será muito ruim. Se você estiver escrevendo um servidor Web C para servir PHP ou Ruby, é muito fácil escrever um servidor mais rápido em javascript ou Ruby ou Python.
Abordagem híbrida
Alguns servidores da web usam uma abordagem híbrida. Nginx e Apache2, por exemplo, implementam seu código de processamento de rede como um pool de threads de loops de eventos. Cada encadeamento executa um loop de eventos simultaneamente processando solicitações de encadeamento único, mas os pedidos são balanceados por carga entre vários encadeamentos.
Algumas arquiteturas de thread único também usam uma abordagem híbrida. Em vez de ativar vários encadeamentos a partir de um único processo, você pode iniciar vários aplicativos - por exemplo, 4 servidores node.js. em uma máquina quad-core. Em seguida, você usa um balanceador de carga para distribuir a carga de trabalho entre os processos.
Com efeito, as duas abordagens são imagens espelhadas tecnicamente idênticas uma da outra.