Concorrência e Processamento Assíncrono
Aprenda como backends lidam com múltiplas tarefas de forma eficiente usando concorrência e modelos de processamento assíncrono.
Por que a Concorrência é Importante em Sistemas de Backend
Sistemas de backend modernos precisam lidar com muitas requisições de entrada ao mesmo tempo; a concorrência é o que torna isso possível.
- Os servidores recebem requisições continuamente de vários usuários e sistemas.
- Processar uma requisição por vez criaria atrasos severos e gargalos.
- A concorrência permite que os sistemas avancem em várias requisições sem esperar que cada uma seja concluída por completo.
Detalhes
Em sistemas reais, as requisições chegam continuamente e muitas vezes em grandes volumes, não uma de cada vez. Um serviço de backend pode receber centenas ou milhares de requisições por segundo, cada uma exigindo computação, acesso ao banco de dados ou chamadas para APIs externas.
Se o servidor processasse essas requisições estritamente em sequência, cada nova requisição teria que esperar a anterior terminar. Isso cria uma fila que cresce rapidamente e leva a uma latência inaceitável para os usuários.
O problema fica pior porque muitas operações envolvem espera, como chamadas de rede ou acesso a disco. Durante esse tempo de espera, a CPU fica ociosa, o que é um uso ineficiente dos recursos.
A concorrência resolve isso permitindo que o sistema sobreponha trabalhos. Enquanto uma requisição está esperando por I/O, outra requisição pode ser processada. Isso mantém o sistema ativo e melhora a taxa de transferência geral.
O resultado é um sistema de backend que consegue atender muitos usuários ao mesmo tempo, mantendo a responsividade, o que é um requisito fundamental para aplicações modernas.
Concorrência vs Paralelismo
Concorrência trata de gerenciar várias tarefas em andamento, enquanto paralelismo trata de executar várias tarefas ao mesmo tempo.
- Concorrência permite que as tarefas avancem alternando entre elas.
- Paralelismo usa múltiplos núcleos de CPU para executar tarefas simultaneamente.
Detalhes
Concorrência foca em como um sistema estrutura e gerencia várias tarefas. Um servidor pode começar a processar uma requisição, pausá-la enquanto espera por I/O e então alternar para outra requisição. As tarefas se revezam no progresso, o que mantém o sistema eficiente mesmo com recursos limitados.
Paralelismo, por outro lado, depende do hardware. Se uma máquina tem vários núcleos de CPU, ela pode executar várias tarefas exatamente ao mesmo tempo. Cada núcleo executa sua própria tarefa de forma independente, aumentando a capacidade total de processamento.
A distinção principal é que concorrência diz respeito a coordenação e escalonamento, enquanto paralelismo diz respeito à execução simultânea. Eles resolvem problemas diferentes, mas muitas vezes são usados juntos.
Por exemplo, um servidor backend pode gerenciar milhares de requisições concorrentes usando técnicas assíncronas, ao mesmo tempo em que distribui o trabalho entre vários núcleos de CPU para obter execução paralela.
Entender essa diferença é importante ao analisar desempenho do sistema, escalabilidade e como diferentes frameworks de backend operam sob carga.
Threads
Uma thread é uma unidade de execução dentro de um processo, e servidores backend tradicionais usam múltiplas threads para lidar com requisições simultaneamente.
Cada faixa executa seu próprio caminho de execução. Mais threads significam mais trabalho simultâneo — até que a CPU ou a memória se tornem o limite.
- Cada thread representa um fluxo independente de execução dentro do servidor.
- Múltiplas threads permitem que o servidor lide com várias requisições ao mesmo tempo.
- Modelos baseados em threads são amplamente usados em frameworks backend como Java Spring Boot.
Detalhes
Um processo é uma instância em execução de um programa, e dentro desse processo, as threads são as unidades que realmente executam o trabalho. Um único processo pode conter várias threads, cada uma executando de forma independente enquanto compartilha o mesmo espaço de memória.
Em sistemas backend tradicionais, as requisições recebidas são atribuídas a threads. Por exemplo, quando uma requisição chega, o servidor pode alocar uma thread de um thread pool para tratá-la. Essa thread processa a requisição do início ao fim, incluindo lógica de negócio, consultas ao banco de dados e geração da resposta.
Esse modelo é direto e fácil de entender. Cada requisição tem sua própria thread, então os fluxos de execução ficam isolados, e os desenvolvedores podem escrever código de forma majoritariamente sequencial.
No entanto, threads não são gratuitas. Cada thread consome memória e adiciona overhead ao sistema. À medida que o número de requisições concorrentes cresce, criar threads demais pode levar à degradação de desempenho devido a context switching e esgotamento de recursos.
Por causa dessas limitações, sistemas modernos frequentemente combinam abordagens baseadas em threads com técnicas assíncronas, mas entender threads continua sendo essencial, já que muitos sistemas em produção ainda dependem desse modelo.
Operações Bloqueantes vs Não Bloqueantes
Operações bloqueantes fazem uma thread esperar até que uma tarefa seja concluída, enquanto operações não bloqueantes permitem que a thread continue fazendo outro trabalho.
Quando uma tarefa espera por I/O, um modelo bloqueante توقف o trabalhador. Sistemas não bloqueantes mantêm a faixa ativa enquanto esperam.
- Operações bloqueantes pausam a execução até que o resultado esteja pronto.
- Operações não bloqueantes permitem que o sistema continue processando outras tarefas.
Detalhes
Em uma operação bloqueante, uma thread inicia uma tarefa e então espera até que essa tarefa seja totalmente concluída antes de seguir em frente. Por exemplo, quando uma thread envia uma consulta ao banco de dados, ela pode ficar ociosa até que o banco responda. Durante esse tempo, a thread não pode realizar nenhum outro trabalho útil.
Isso se torna uma grande limitação em sistemas que lidam com muitas requisições. Se cada thread passa uma quantidade significativa de tempo esperando por I/O, o servidor precisa de mais threads para manter a taxa de processamento, o que aumenta o uso de recursos e o overhead.
Operações não bloqueantes adotam uma abordagem diferente. Em vez de esperar, a thread inicia a tarefa e imediatamente continua executando outro trabalho. Quando o resultado fica pronto, o sistema é notificado, e a tarefa original pode ser retomada.
Isso permite que uma única thread gerencie várias tarefas de forma eficiente, especialmente em cargas de trabalho intensivas em I/O, nas quais o tempo de espera domina o tempo de execução.
Entender a diferença entre comportamento bloqueante e não bloqueante é fundamental, porque isso influencia diretamente como sistemas backend são projetados para desempenho, responsividade e escalabilidade.
E/S assíncrona
A E/S assíncrona permite que os servidores continuem processando outras tarefas enquanto aguardam operações lentas, como consultas ao banco de dados ou chamadas de rede.
- Muitas operações de backend envolvem espera por sistemas externos, como bancos de dados ou APIs.
- A E/S assíncrona impede que threads fiquem ociosas durante essas esperas.
- Essa abordagem melhora a taxa de processamento e a eficiência de recursos.
Detalhes
Em sistemas de backend, muitas operações não são limitadas por CPU, mas por E/S. Tarefas como consultar um banco de dados, chamar uma API externa ou ler do disco podem levar muito mais tempo do que o processamento em memória.
Se essas operações forem tratadas de forma bloqueante, a thread permanece ociosa enquanto espera pelo resultado, o que desperdiça recursos do sistema e limita quantas requisições o servidor consegue atender.
A E/S assíncrona resolve isso permitindo que o servidor inicie uma operação e depois siga para outro trabalho em vez de esperar. O sistema acompanha a operação pendente e retoma o processamento assim que o resultado estiver disponível.
Isso permite que uma única thread ou um pequeno número de threads gerencie muitas requisições concorrentes de forma eficiente, tornando a E/S assíncrona uma técnica central em sistemas de backend de alto desempenho.
Modelo de Event Loop
O modelo de event loop usa um pequeno número de threads para lidar com muitas requisições, processando tarefas de forma assíncrona em vez de dedicar uma thread por requisição.
Cada requisição ocupa seu próprio trabalhador.
Um loop percorre muitas tarefas.
O loop de eventos despacha trabalho pesado para threads.
- Um único event loop processa continuamente tarefas de uma fila.
- Operações não bloqueantes permitem que o sistema evite esperar.
- Callbacks são executados quando as operações assíncronas são concluídas.
Detalhes
No modelo de event loop, o sistema não atribui uma thread separada para cada requisição. Em vez disso, um loop central verifica continuamente novas tarefas e as executa uma por vez. Quando uma requisição chega, o event loop começa a processá-la e inicia quaisquer operações de I/O necessárias de forma não bloqueante.
Em vez de esperar que essas operações sejam concluídas, o event loop segue para lidar com outras requisições recebidas. Isso mantém o sistema ocupado e evita desperdiçar tempo esperando ocioso, algo comum em modelos baseados em threads.
Quando uma operação assíncrona termina, seu resultado é colocado em uma fila de callbacks. O event loop eventualmente pega esse callback e conclui o trabalho restante dessa requisição.
Um exemplo conhecido desse modelo é o Node.js. Ele usa um event loop de thread única combinado com I/O assíncrono para lidar com milhares de conexões concorrentes de forma eficiente, contando com callbacks e promises para gerenciar o fluxo de execução.
Essa abordagem é altamente eficiente para cargas de trabalho intensivas em I/O, mas exige um design cuidadoso para evitar bloquear o event loop, já que uma única tarefa lenta pode atrasar todas as outras.
Condições de corrida
Uma condição de corrida ocorre quando várias threads acessam e modificam dados compartilhados ao mesmo tempo, levando a resultados imprevisíveis e incorretos.
- Condições de corrida acontecem quando várias threads operam sobre dados compartilhados sem coordenação adequada.
- O resultado final depende do timing e da ordem de execução.
- Elas são uma grande fonte de bugs em sistemas concorrentes.
Detalhes
Uma condição de corrida surge quando duas ou mais threads leem e atualizam a mesma parte dos dados ao mesmo tempo. Como essas operações não são sincronizadas, o resultado depende de qual thread executa primeiro, tornando o comportamento do sistema imprevisível.
Por exemplo, se duas threads lerem o mesmo saldo de conta e ambas tentarem atualizá-lo, uma atualização pode sobrescrever a outra. Isso resulta em dados incorretos, mesmo que cada operação individual pareça correta isoladamente.
Esses problemas são difíceis de detectar porque podem não ocorrer de forma consistente. Pequenas diferenças de timing podem alterar a ordem de execução, causando bugs que são difíceis de reproduzir e depurar.
Para evitar condições de corrida, os sistemas usam técnicas de sincronização como locks, atomic operations e database transactions. Esses mecanismos garantem que apenas uma thread possa modificar dados compartilhados por vez ou que as operações sejam executadas com segurança, sem interferência.
Seção de Perguntas
1 / 5
Esta lição faz parte do conteúdo premium
Faça upgrade para o plano premium para remover o desfoque e liberar a leitura completa.