DEV Community

Cover image for Entendendo Sícrono vs Assíncrono: Threads, Await e Performance
chihiro
chihiro

Posted on

Entendendo Sícrono vs Assíncrono: Threads, Await e Performance

Conceitos como esses inevitavelmente aparecem na vida de quem desenvolve software. No começo, tudo parece confuso — e é normal. Você não precisa entender tudo de uma vez, quase ninguém entende. Mas maturar esses temas ao longo do tempo, sem fugir deles por medo da complexidade, é essencial.

Você já se perguntou por que alguns aplicativos travam enquanto carregam algo, enquanto outros continuam funcionando normalmente? Ou por que certos servidores conseguem atender milhares de usuários ao mesmo tempo, enquanto outros ficam lentos com apenas dezenas? A resposta está na forma como o código lida com operações demoradas — e é isso que vamos entender agora.

Neste artigo, quero esclarecer esses conceitos de forma direta e prática, para que sua jornada seja mais tranquila desde o início.


Antes de começar: o que é uma thread?

Uma thread é como um "funcionário" do seu programa: ela pega uma tarefa, executa do início ao fim, e só depois pega outra. Seu servidor tem um número limitado dessas "threads trabalhadoras" disponíveis.

Pense assim: se seu servidor tem 10 threads, ele consegue processar até 10 tarefas simultaneamente. Quando todas estão ocupadas, novas requisições precisam esperar na fila.


🔵 1. Execução Síncrona: um passo de cada vez

Na execução síncrona, o fluxo é linear:

  1. Inicia uma tarefa
  2. Espera concluir
  3. Só então passa para a próxima

É simples e previsível. Porém, carrega uma limitação importante: ele bloqueia o andamento do restante do sistema.

O que significa "bloquear"?

🚫 Operação bloqueante = a thread fica "travada", esperando a resposta
✅ Operação não-bloqueante = a thread pode fazer outras coisas enquanto espera

Enter fullscreen mode Exit fullscreen mode

Um jeito fácil de visualizar

Pense em uma padaria onde o caixa atende uma única pessoa por vez.

Enquanto o cliente atual não finalizar a compra, ninguém mais avança na fila.

Na programação, o efeito é idêntico: se uma operação leva 3 segundos, a thread responsável por ela fica ocupada por 3 segundos — e nada mais anda nesse tempo.

Requisição 1: [████████████] 3s → Thread ocupada
Requisição 2:              [████████████] 3s → Aguardando...
Requisição 3:                           [████████████] 3s → Aguardando...

Enter fullscreen mode Exit fullscreen mode

Quando o modelo síncrono funciona bem

Ele é ideal para tarefas rápidas e diretas, como validações e operações puramente locais:

if not payload.email:
    raise ValueError()

Enter fullscreen mode Exit fullscreen mode

Essas ações são imediatas e não justificam mecanismos mais complexos.

Onde começam os problemas

O desempenho degrada quando o fluxo envolve operações lentas ou externas, como:

  • chamadas de rede
  • acesso a disco
  • processamento pesado de CPU

Imagine uma rota que precisa:

  1. Receber a requisição
  2. Buscar informações no banco
  3. Gerar um relatório pesado (PDF/planilha)
  4. Salvar o arquivo
  5. Só então enviar a resposta
@app.get("/relatorio")
def gerar_relatorio():
    dados = buscar_dados_no_banco()     # I/O de rede — lento e bloqueante
    pdf = gerar_pdf(dados)              # CPU intensiva — bloqueante
    salvar_no_storage(pdf)              # I/O de disco — bloqueante
    return {"status": "pronto"}

Enter fullscreen mode Exit fullscreen mode

Durante toda essa sequência, a mesma thread permanece ocupada:

  • esperando o banco responder
  • gerando o PDF
  • salvando o arquivo

Enquanto isso acontece, ela não pode atender mais nada. Em um servidor com 10 threads, se 10 requisições pesadas chegarem ao mesmo tempo, o servidor trava a capacidade de atendimento até que alguma thread fique livre.

O impacto em números

Imagine que cada etapa demore 1 segundo:

  • Buscar dados: 1s
  • Gerar PDF: 1s
  • Salvar arquivo: 1s
  • Total: 3 segundos por requisição

Com 10 threads disponíveis, você atende no máximo 10 requisições simultaneamente. A 11ª pessoa precisa esperar 3 segundos inteiros antes de ser atendida. Se 100 pessoas acessam ao mesmo tempo, as últimas aguardam até 30 segundos!


🟢 2. Execução Assíncrona: avance enquanto a tarefa acontece

Na execução assíncrona, o programa não fica parado esperando tarefas lentas terminarem.

Ele inicia uma operação, libera a thread e segue adiante.

Quando o resultado chega, a execução retoma de onde parou.

Um jeito fácil de visualizar

Imagine que você está na mesma padaria — mas agora ela trabalha com senha eletrônica e múltiplos atendimentos em paralelo.

Você faz o pedido, recebe uma senha e é liberado para esperar.

Enquanto isso, o caixa pode atender outras pessoas.

Quando o seu pedido fica pronto, o sistema te chama:

"Pedido 37, favor retirar".

No mundo da programação, é exatamente isso que o modelo assíncrono faz: ele não bloqueia enquanto o trabalho está sendo feito.

Como isso funciona na prática

Em vez de ocupar uma thread esperando uma operação lenta — como consulta ao banco ou chamada externa — o código "dispara" a tarefa e continua trabalhando.

Quando a operação termina, um evento ou callback avisa:

"ei, já terminei, pode continuar!".

Exemplo real com async/await

Aqui está a mesma rota de geração de relatório, mas escrita de forma assíncrona:

@app.get("/relatorio")
async def gerar_relatorio():
    dados = await buscar_dados_no_banco_async()  # libera a thread enquanto o DB responde
    pdf = await gerar_pdf_async(dados)           # libera a thread enquanto processa
    await salvar_no_storage_async(pdf)           # libera a thread enquanto salva
    return {"status": "pronto"}

Enter fullscreen mode Exit fullscreen mode

Observe o que muda:

  • Nenhuma operação trava a thread principal
  • A thread é "devolvida" ao servidor enquanto a tarefa está em andamento
  • Outras requisições podem ser atendidas nesse intervalo

🔍 O que o await realmente faz?

Ele é como um "pausa inteligente":

  • Sinaliza: "essa operação vai demorar, não me espere aqui"
  • Libera a thread para fazer outras coisas
  • Quando a operação termina, retoma exatamente de onde parou

É diferente de simplesmente "esperar" — é esperar sem desperdiçar recursos.

O ganho em números

Com o mesmo cenário anterior (operações de 1s cada):

  • O servidor inicia a consulta ao banco e libera a thread
  • Enquanto o banco processa, essa mesma thread atende outras 10 pessoas
  • Quando o banco responde, o servidor retoma de onde parou
  • Resultado: 100+ requisições sendo processadas "ao mesmo tempo" com apenas 10 threads!

Isso permite que um único servidor lide com muito mais requisições simultâneas.

O que acontece com as threads?

Em sistemas assíncronos, você não precisa de "1 thread por requisição".

Em vez disso:

1 thread → milhares de requisições aguardando respostas

Isso é possível porque as threads só trabalham quando têm algo de fato para executar — não ficam bloqueadas esperando respostas externas.

Requisição 1: [██--await--██--await--██]
Requisição 2:   [██--await--██--await--██]
Requisição 3:     [██--await--██--await--██]
                    ↑ Thread livre entre operações

Enter fullscreen mode Exit fullscreen mode

🟡 3. E as filas de mensagens?

Filas (como RabbitMQ, Redis Queue, AWS SQS) são outra forma de lidar com operações lentas — e complementam muito bem os modelos anteriores:

  1. A requisição chega
  2. Você envia a tarefa para uma fila
  3. Responde imediatamente: "ok, vou processar isso"
  4. Um worker consome a fila e executa quando puder

Quando usar filas?

  • Quando você pode responder "recebi!" sem precisar do resultado na hora
  • Para processos que podem ser executados em background
  • Quando precisa garantir que tarefas não sejam perdidas mesmo se o servidor cair

Exemplo prático:

@app.post("/enviar-email")
async def enviar_email(destinatario: str, mensagem: str):
    # Em vez de enviar o email agora (lento)
    fila.adicionar_tarefa({
        "tipo": "email",
        "destinatario": destinatario,
        "mensagem": mensagem
    })
    return {"status": "email agendado"}  # Resposta instantânea!

Enter fullscreen mode Exit fullscreen mode

Enquanto isso, workers separados processam a fila em segundo plano.


⚠️ 4. Quando o assíncrono NÃO resolve

Operações que realmente usam CPU continuamente (como processar imagens, cálculos matemáticos pesados, encoding de vídeo) não se beneficiam muito do async/await. Por quê?

Porque async funciona bem para operações de espera (I/O), não para operações que exigem processamento contínuo.

Exemplo:

# Isso NÃO vai melhorar com async
async def processar_video(video):
    await converter_codec(video)  # Ainda usa 100% da CPU
    # A thread não fica "livre" porque a CPU está trabalhando full time

Enter fullscreen mode Exit fullscreen mode

Para esses cenários, considere:

  • Usar workers em processos separados
  • Delegar para serviços especializados
  • Usar múltiplos servidores/containers

🎯 Como decidir na prática

Use execução síncrona quando:

  • A operação é rápida (< 100ms)
  • O código é simples e direto
  • Não há chamadas externas ou I/O pesado
  • Você está fazendo validações ou cálculos leves

Use execução assíncrona quando:

  • Faz chamadas a APIs, bancos de dados ou serviços externos
  • Precisa lidar com muitas requisições simultâneas
  • O tempo de resposta varia e pode ser longo
  • Suas operações passam mais tempo "esperando" do que "processando"

Use filas quando:

  • A resposta não precisa ser imediata
  • Quer garantir que tarefas não sejam perdidas
  • Precisa controlar a taxa de processamento
  • O trabalho pode ser feito em background

E lembre-se: não precisa escolher apenas um. Sistemas reais frequentemente combinam todos esses modelos, usando cada um onde faz mais sentido.


Conclusão

A execução síncrona e a execução assíncrona resolvem problemas diferentes e, por isso, ambas têm seu lugar no desenvolvimento moderno.

O modelo síncrono é direto, fácil de entender e funciona muito bem para tarefas rápidas e previsíveis. Mas, à medida que o sistema começa a lidar com operações lentas — como chamadas externas, processamento pesado ou acesso a disco — ele rapidamente se torna um gargalo. Cada thread ocupada é uma requisição que deixa de ser atendida.

A execução assíncrona surge justamente para evitar esse desperdício. Ela permite que o servidor continue trabalhando enquanto tarefas demoradas acontecem em segundo plano, liberando threads, aumentando o throughput e tornando o sistema muito mais escalável.

No fim, compreender a diferença entre esses modelos não é apenas um detalhe técnico: é uma habilidade essencial para arquitetar APIs, microserviços e aplicações modernas que precisam lidar com alto volume de tráfego e eficiência.

Se o sincronismo garante simplicidade, o assincronismo entrega performance.

E o papel do desenvolvedor é saber qual modelo escolher — e quando.

Top comments (0)