Trilha 09 · Concorrência e paralelismo

Async, event loop e concorrência cooperativa

Threads não são a única forma de concorrência. O modelo assíncrono usa um único thread com um event loop: quando uma operação de I/O começa, a tarefa suspende e outra roda. Sem locks, sem race conditions — mas com uma pegadinha: código CPU-bound bloqueia tudo.

① Intuição

Um garçom atendendo múltiplas mesas

O modelo de threads é como ter N garçons (um por mesa): trabalham em paralelo mas precisam coordenar o uso da cozinha (mutex). O modelo assíncrono é como um garçon eficiente: anota o pedido (inicia I/O), vai atender outra mesa enquanto a cozinha prepara, e volta quando está pronto. Um garçon, N mesas — concorrência sem paralelismo.

async/await é "concorrência cooperativa": a tarefa voluntariamente cede controle ao event loop quando precisa esperar. O event loop escolhe a próxima tarefa pronta. Não há preempção involuntária — o código entre dois await roda sem interrupção.

Quando usar threads vs async? Use async para I/O-bound com muitas conexões simultâneas (servidor web, scraper, bot de Telegram). Use threads quando bibliotecas blocking não têm versão async. Use multiprocessing para CPU-bound (processamento de imagem, ML, criptografia). Go usa goroutines que combinam o melhor dos dois: leves como async, paralelas como threads.
② Visualização interativa

O event loop do JavaScript passo a passo

Veja como o código síncrono, Promises (microtasks) e setTimeout (macrotasks) são processados em ordens diferentes pelo event loop.

CÓDIGO
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
CALL STACK
main()
MICROTASKS
vazia
TASK QUEUE
vazia
OUTPUT
Script começa a executar. main() está no call stack.
1/8
③ Explicação técnica

async/await em Python

# Python async/await — concorrência cooperativa sem threads
import asyncio

async def fetch_url(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    urls = ["https://api.site.com/1", "https://api.site.com/2", ...]
    # gather: executa todas as coroutines concorrentemente
    results = await asyncio.gather(*[fetch_url(u) for u in urls])
    return results

# await suspende esta coroutine enquanto espera I/O
# O event loop roda outra coroutine enquanto espera
# Tudo em 1 thread — sem race conditions!
# Mas: uma operação CPU-bound bloqueia TUDO

asyncio.run(main())   # Python 3.7+

Event loop do JavaScript e goroutines do Go

// JavaScript: o event loop é a base de tudo no Node.js

console.log('A');             // 1. síncrono — executa imediato

setTimeout(() => {             // 2. macrotask: timer API
    console.log('B');         //    cb_B → Task Queue após 0ms
}, 0);

Promise.resolve().then(() => { // 3. microtask: promise
    console.log('C');         //    cb_C → Microtask Queue (imediato)
});

console.log('D');             // 4. síncrono

// Saída: A D C B
// Ordem: síncrono → microtasks → (próxima iteração) macrotasks
// Promessas têm prioridade sobre setTimeout!

// Go: goroutines — threads leves do runtime Go
go func() {
    fmt.Println("goroutine rodando")   // thread leve (2KB stack inicial)
}()
// Runtime Go gerencia milhares de goroutines em poucos OS threads
// Comunicação via canais (channels), não memória compartilhada
Microtasks vs Macrotasks (JavaScript): o event loop do JS tem duas filas. Microtasks (Promise.then, queueMicrotask) são drenadas completamente antes do próximo macrotask (setTimeout, setInterval, I/O callbacks). Isso garante que Promises resolvem antes do próximo "tick". Uma cadeia infinita de .then() pode bloquear a renderização — é o "starvation de microtasks".
④ Projeto para programar

Construindo servidores assíncronos

Mini projeto: use asyncio para fazer 10 requisições HTTP em paralelo para a API pública jsonplaceholder.typicode.com. Compare o tempo com a versão sequencial (requests simples). Adicione um semáforo assíncrono (asyncio.Semaphore(3)) para limitar a 3 requisições simultâneas.

Projeto principal: implemente um servidor HTTP simples com asyncio que serve arquivos estáticos. Use asyncio.start_server. Simule 1000 conexões simultâneas com asyncio.gather. Compare com um servidor threading que cria uma thread por conexão — memória usada, latência, throughput.

Desafio extra: implemente um chat server em Go usando goroutines e channels: cada cliente tem uma goroutine para leitura e uma para escrita; um "hub" goroutine gerencia broadcast com um canal. Compare com a implementação em Python asyncio. Explique a diferença entre comunicação via channels (CSP) e memória compartilhada com locks.

⑤ Exercícios rápidos

Teste sua intuição

Quando async/await tem vantagem sobre threads?
Qual é a ordem de execução: console.log('A'), setTimeout(cb, 0), Promise.resolve().then(cb2)?
O que torna goroutines do Go mais leves que threads do SO?
⑥ Aplicações no mundo real

Onde você encontra isso

Node.js — 1 thread, milhões de conexões

O Node.js usa um único thread com event loop (libuv). Isso é suficiente para servir dezenas de milhares de conexões simultâneas porque a maioria espera I/O (rede, banco, disco). A crítica histórica era que código CPU-bound bloqueava tudo — resolvido com Worker Threads (Node 12+) e wasm para cálculos pesados. Discord usa Node.js e serve milhões de usuários simultâneos.

🐍

FastAPI — async Python moderno

FastAPI (Python) usa asyncio e Starlette para servir requisições HTTP assíncronas. Com uvicorn (servidor ASGI), um único processo serve centenas de requisições simultâneas sem threads. Benchmark: FastAPI supera Flask em throughput porque evita o overhead de criar threads por requisição. É o framework Python mais rápido para APIs, usado por Netflix, Microsoft e Uber.

🦀

Tokio — async runtime para Rust

Tokio é o runtime assíncrono mais popular do Rust: implementa o event loop, agendador de tarefas e I/O não-bloqueante. Programas Rust com Tokio usam zero threads do SO para concorrência de I/O — tudo é gerenciado pelo runtime em poucos threads. O servidor web Axum sobre Tokio é um dos mais rápidos em benchmarks TechEmpower, competindo com C e Go.

← Anterior: Filósofos e deadlock Próxima: Lei de Amdahl →