trilha

System Design/03 - Qualidade/Performance Antipatterns2 min

Synchronous IO

Perguntas-guia
  • Que problema isso resolve?
  • Quando usar / quando NÃO usar?
  • Qual o principal trade-off?
  • Como isso falha em produção?

Conceito

Bloquear uma thread esperando I/O — disco, rede, banco — quando ela poderia estar atendendo outra requisição.

Durante a espera, a thread não usa CPU: ela apenas ocupa memória (pilha de ~1 MB) e uma vaga no pool. Com I/O de 100 ms e um pool de 200 threads, o teto é 2.000 requisições por segundo, independentemente de quanta CPU exista sobrando.

O sintoma característico

CPU:      15%
Memória:  40%
Latência: 4 segundos

Baixa utilização de recurso com alta latência é a assinatura do problema: o sistema está esperando, não trabalhando.

As saídas

Modelo Como funciona
Async / await A thread é liberada durante a espera
Event loop Uma thread, milhares de conexões (Node, nginx)
Corrotinas / fibers Threads leves (Go, Kotlin, Java 21 virtual threads)
Reactive streams Fluxo não-bloqueante com back pressure

Trade-offs

Código assíncrono é mais difícil de escrever, ler e depurar. Stack traces ficam fragmentados, o fluxo deixa de ser linear, e erros de concorrência são mais sutis. O "color problem" (funções assíncronas contaminam quem as chama) força mudanças em cascata.

Há também um limite importante: assincronia não ajuda em trabalho de CPU. Se a thread está calculando, liberá-la não é possível — o gargalo é CPU, e a resposta é paralelismo ou mais máquinas.

E um erro comum é a assincronia parcial: um único ponto bloqueante dentro de um event loop trava tudo. Uma chamada síncrona ao disco num servidor Node paralisa todas as conexões que aquele loop atende.

Virtual threads (Java 21) e goroutines mudam o cálculo: entregam o ganho de escalabilidade mantendo o código com aparência síncrona — o melhor dos dois mundos, quando disponíveis na plataforma.

Exemplo prático

# bloqueante: a thread fica parada 300 ms
def handler():
    a = requests.get(url_a)     # 100 ms parado
    b = requests.get(url_b)     # 100 ms parado
    c = db.query(sql)           # 100 ms parado
    return combinar(a, b, c)

# assíncrono e paralelo: 100 ms, thread livre durante a espera
async def handler():
    a, b, c = await asyncio.gather(
        fetch(url_a), fetch(url_b), query(sql))
    return combinar(a, b, c)

Duas melhorias distintas acontecem aqui. A paralelização reduz a latência de 300 ms para 100 ms — ganho visível ao usuário. A não-blocagem libera a thread durante a espera — ganho invisível ao usuário e decisivo para o throughput: a mesma máquina passa a atender ordens de grandeza mais requisições simultâneas.

Relacionado


Parte de Performance Antipatterns · roadmap.sh/system-design

Buscar

Busca por título, seção e texto das notas