Treze conceitos depois, o módulo se fecha com a pergunta que resume todo o resto: dado um problema concorrente, qual modelo você escolhe? Async/await, goroutines, threads tradicionais, multiprocessing, actors, virtual threads, channels — você viu o catálogo inteiro. Cada um tem casos onde brilha e casos onde atrapalha. A intuição que separa sêniores dos demais é saber classificar o problema antes de escrever código, e escolher ferramenta adequada à natureza do trabalho.
A pergunta central é sempre uma versão de: onde o tempo vai?. Se a maior parte do tempo é gasta esperando — rede, banco, disco, API externa, um humano clicando — você tem um problema I/O-bound, e os modelos cooperativos (async/await, goroutines, asyncio) são o caminho. Se a maior parte é gasta calculando — comprimir vídeo, treinar modelo, fazer parse pesado, criptografar grandes volumes — você tem um problema CPU-bound, e a única forma de acelerar é dividir trabalho entre cores reais (paralelismo). A confusão entre os dois é a causa mais comum de decisões arquiteturais que envelhecem mal.
Pike, em sua palestra de 2012 que o módulo abriu citando, não estava sendo pedante: a distinção concorrência vs paralelismo organiza essa decisão. Concorrência é o modelo de programa (estrutura). Paralelismo é o uso de hardware (execução). Para I/O-bound, concorrência sem paralelismo (asyncio em Python, Node, single-thread async) já entrega a maior parte do ganho. Para CPU-bound, você precisa de fato de paralelismo (multi-core em uso simultâneo). Tentar resolver CPU-bound com asyncio é desperdício; tentar resolver I/O-bound massivo com threads do SO é desperdício no sentido oposto.
Este conceito de fechamento traz a árvore de decisão completa. Como medir natureza do trabalho. Como escolher modelo. Como combinar modelos quando o problema é misto (a maioria dos casos reais). E como avaliar trade-offs entre simplicidade, performance e custo operacional.
I/O-bound vs CPU-bound — definição operacional
A distinção formal é simples: trabalho é I/O-bound se o tempo de execução é dominado por espera em recursos externos; CPU-bound se é dominado por processamento que mantém o processador ocupado. Operacionalmente, a fronteira fica clara quando você profila. Em CPU-bound, a função roda quase 100% de CPU enquanto executa. Em I/O-bound, a função passa a maior parte do wall-clock parada — bloqueada em syscall ou aguardando rede.
Exemplos típicos de I/O-bound:
- HTTP requests para APIs externas — 99% do tempo é rede.
- Queries em banco — driver bloqueia esperando resposta.
- Leitura/escrita de arquivos — esperando disco/SSD.
- Web scraping — fetch domina; parse é negligível.
- WebSocket servers — quase tudo é esperar mensagem.
- Upload/download de arquivos — limitado por banda.
Exemplos típicos de CPU-bound:
- Compressão (gzip, brotli) — algoritmo intensivo.
- Encoding de mídia (FFmpeg, libx264) — saturado em cores.
- Treinamento e inferência de modelos ML (sem GPU) — cálculo bruto.
- Hash criptográfico de grandes arquivos — saturado.
- Parse complexo (XML grande, regex pesado) — algoritmo cara.
- Image processing (resize, filters) — pixel por pixel.
- Simulações numéricas, computação científica.
Casos mistos são o padrão em sistemas reais. Servidor que recebe JSON (I/O), parseia (CPU breve), valida (CPU breve), consulta banco (I/O), faz cálculo (CPU médio), retorna (I/O). Sob carga, qual fase domina? Tipicamente uma — o gargalo. E essa fase determina escolha de modelo.
Como medir — a escolha não é por intuição
Sêniores não decidem por intuição. Eles medem. Os instrumentos certos para classificar trabalho:
CPU usage durante a operação
Em Linux, top -H -p PID mostra CPU per-thread em
tempo real. Se uma thread gasta 100% de um core enquanto roda,
é CPU-bound. Se gasta 5% e está sleeping/waiting o resto, é
I/O-bound. htop com H ligado mostra o
mesmo visualmente.
Profilers
perf em Linux: perf record + perf
report mostra distribuição de CPU. Função que aparece como
consumindo muito CPU é candidata a CPU-bound. Função que aparece
como negligível em CPU mas o programa demora muito é I/O-bound.
Linguagens têm equivalentes: py-spy (Python),
pprof (Go), Visual Studio Profiler (C#),
async-profiler (Java). Flame graphs (Brendan Gregg)
tornam o resultado visualmente óbvio.
Wall-clock vs CPU-time
Trick útil: cronometrar a operação duas vezes — wall-clock
(time.Now, time.perf_counter) e
CPU-time (os.process_time, ou subtração de
getrusage). Se wall-clock >> CPU-time, é
I/O-bound (o tempo extra foi gasto bloqueado). Se são próximos,
é CPU-bound.
# Python
import time, os
def medir(operacao):
wall_start = time.perf_counter()
cpu_start = time.process_time()
operacao()
wall = time.perf_counter() - wall_start
cpu = time.process_time() - cpu_start
print(f"wall: {wall:.2f}s cpu: {cpu:.2f}s ratio: {cpu/wall:.1%}")
medir(buscar_url) # wall: 0.5s, cpu: 0.01s, ratio: 2% → I/O-bound
medir(comprimir_video) # wall: 30s, cpu: 29s, ratio: 97% → CPU-bound
Heurística rápida
Para classificar sem instrumentação completa: pergunte "se eu rodar isso em uma máquina com 100x mais CPU, fica 100x mais rápido?". Se sim (no limite teórico), é CPU-bound. Se a resposta é "não, ainda demora porque depende de rede/disco", é I/O-bound. Em casos mistos, "rodaria 2-3x mais rápido" — você tem CPU-bound parcial.
Escolha de modelo — uma escada
A escolha cai em quatro perguntas em sequência. Cada resposta elimina algumas opções e aponta outras.
1. Há concorrência genuína a explorar?
Se o problema é estritamente sequencial (cada passo depende do anterior), concorrência não ajuda. Adicionar threads/async em problema sequencial só adiciona overhead. Resposta: código síncrono normal.
2. Trabalho é I/O-bound ou CPU-bound?
Esta é a pergunta de classificação que dominou este conceito até aqui. Tipicamente clara após medição de CPU usage / wall vs CPU time.
3. Quantas operações simultâneas?
Em I/O-bound: dezenas? threads tradicionais bastam. Centenas? pool de threads ou async começa a fazer diferença. Milhares ou milhões? só modelos cooperativos cabem (asyncio, goroutines, virtual threads, actors). A escala muda a resposta.
Em CPU-bound: a escala é limitada por número de cores. 8 cores = no máximo 8 unidades de paralelismo útil para CPU pura. Adicionar mais workers só introduz contention.
4. Há requisitos especiais?
Real-time? sistema distribuído? localidade transparente? cada um pesa modelos diferentes (lock-free para real-time; actor model para distribuído com identidade). Casos especiais escolhem ferramenta especial.
Modelos por categoria de trabalho
I/O-bound, alta concorrência (10K+ operações)
Caso clássico: web server, scraper, gateway, proxy. Modelos cooperativos vencem por economia de memória e overhead. asyncio em Python, async/await em C#/Node/Rust, goroutines em Go, virtual threads em Java Loom. Para 100K conexões em servidor, threads tradicionais do SO custariam ~800 GB de stack virtual; asyncio cabe em poucos MB. A diferença é arquitetural.
I/O-bound, baixa concorrência (dezenas)
Worker que processa jobs com chamadas a APIs externas, em paralelismo modesto. Aqui qualquer modelo serve — threads tradicionais, async, goroutines. Não há ganho mensurável de modelos cooperativos em escala pequena. Escolha por ergonomia da linguagem ou ecossistema.
CPU-bound em multi-core
Compressão, encoding, ML inference, simulação. Dividir o trabalho
entre threads do SO em cores físicos (não lógicos —
hyperthreading não ajuda em CPU puro). O modelo é
thread pool com tamanho ≈ cores físicos.
Parallel.ForEach em C#, multiprocessing.Pool
em Python, rayon em Rust. Em Go, goroutines com
runtime.NumCPU() e channels. Async/await aqui é
contraproducente — você só serializa CPU-bound em um thread.
CPU-bound em Python — caso especial
O GIL torna threading de Python ineficaz para CPU-bound. As opções:
- multiprocessing: fork de processos separados, cada um com GIL próprio. Comunicação custosa via IPC (pickle).
- extensões C com nogil: NumPy, SciPy, Pandas, Polars liberam GIL em código nativo. Multi-thread em Python pode acelerar se a hot path está em código C que solta GIL.
- Python 3.13 free-threaded (no-GIL): experimental desde out/2024. Em 2026, ecosystem ainda se adapta. Para código novo greenfield, vale considerar; para legacy, espere maturidade.
- Cython com nogil: escrever hot path em
Cython e marcar
nogil. - Mude de linguagem para o hot path: Python para a aplicação, Rust ou Go para a parte CPU pesada, invocada via FFI ou serviço. Padrão comum em ML serving.
Mixed — combinando modelos
Sistemas reais raramente são puramente um ou outro. Servidor web que processa JSON pesado: I/O-bound no recebimento + CPU-bound no parse + I/O-bound no banco. A solução é combinar modelos:
- Modelo principal cooperativo (asyncio, async/await, goroutines) para a maior parte das operações I/O-bound.
- Pool dedicado de threads/processos para o trabalho
CPU-bound. Em Python:
asyncio.run_in_executorcomProcessPoolExecutor. Em C#:Task.Rundentro de async. Em Go: usualmente goroutines bastam (runtime cuida); para CPU-bound intenso, considere serializar com pool. - Cuidado para não bloquear o event loop com CPU-bound síncrono sem isolar.
Escolha de runtime — qual linguagem para qual problema
Não é só sobre o modelo dentro de uma linguagem; é sobre qual linguagem escolher quando o problema permite escolha. Em 2026, o cenário pragmático:
Go
Goroutines + scheduler M:N gratuito. I/O e CPU em mesmo modelo, sem distinção sintática. Asincronicidade implícita; sem "cor de função". Excelente para ambos. Usado por toda infraestrutura cloud-native (Kubernetes, Docker, Cloudflare). Para sistema híbrido novo greenfield com requisitos altos de concorrência, Go é candidato natural.
Java + Loom (JDK 21+)
Virtual threads desde 2023 colocam Java no nível de Go para I/O-bound massivo. Threads tradicionais ainda existem para CPU-bound onde paralelismo de cores é o que importa. Ecosystem maduro, JVM excelente em CPU-bound. Para sistemas com base Java existente, Loom torna concorrência muito mais fácil sem reescrita.
C# / .NET
async/await idiomático para I/O. Parallel,
Task.Run, Parallel.ForEachAsync para
CPU. ThreadPool gerenciado é excepcional. .NET 8+ e CoreCLR
estão entre as runtimes mais rápidas em geral. Forte para
backend de aplicação misto.
Python
Forte para I/O via asyncio (FastAPI, aiohttp, asyncpg). Limitado em CPU-bound puro pelo GIL — mitigado por NumPy/Polars em data, multiprocessing em geral, ou no-GIL em 3.13+. Para científico, ML, scripting, data engineering, é primeira escolha mesmo com as limitações. Para sistemas onde CPU domina e escalar é crítico, considere outra linguagem para o hot path.
Node.js
Excelente em I/O via event loop libuv. CPU-bound exige
worker_threads separados. Ecossistema rico para
web. Para microsserviços I/O-bound (gateways, BFFs), continua
sólido em 2026.
Rust + tokio
Async/await com runtime tokio para I/O. Threads + Rayon para CPU. Sem GIL, sem GC pauses, performance previsível. Curva de aprendizagem mais íngreme mas resultado é o mais previsível em latência tail. Para sistemas onde performance é diferenciador, Rust é a escolha mais comum em 2026 (Cloudflare Workers, Discord, partes de Linux kernel).
O mesmo problema misto nas três linguagens
Para concretizar combinação de modelos, considere o cenário: receber lista de URLs de imagens, baixar cada uma (I/O), aplicar filtro de processamento (CPU), salvar resultado (I/O). I/O-bound no fetch e save; CPU-bound no filtro. Volume: 1000 imagens.
using System.Net.Http;
async Task ProcessarImagensAsync(string[] urls, CancellationToken ct) {
using var http = new HttpClient();
var paraleloIO = 50; // alto para I/O
var paraleloCPU = Environment.ProcessorCount; // = cores físicos
var sem = new SemaphoreSlim(paraleloIO);
var cpuOpts = new ParallelOptions {
MaxDegreeOfParallelism = paraleloCPU,
CancellationToken = ct
};
await Parallel.ForEachAsync(urls, cpuOpts, async (url, ct2) => {
await sem.WaitAsync(ct2);
byte[] bytes;
try {
bytes = await http.GetByteArrayAsync(url, ct2);
} finally {
sem.Release();
}
// Aplicar filtro em thread pool (CPU-bound)
byte[] filtrado = await Task.Run(() => AplicarFiltro(bytes), ct2);
await SalvarAsync(filtrado, ct2);
});
}
Parallel.ForEachAsync com
MaxDegreeOfParallelism = cores limita o
paralelismo CPU (filtro). SemaphoreSlim
separadamente limita concorrência I/O para 50.
Task.Run joga o filtro CPU-bound para o
ThreadPool, evitando bloquear o context async do download.
import asyncio
import aiohttp
from concurrent.futures import ProcessPoolExecutor
import os
async def processar_imagens(urls: list[str]):
paralelo_io = 50
paralelo_cpu = os.cpu_count() or 4
sem_io = asyncio.Semaphore(paralelo_io)
loop = asyncio.get_running_loop()
pool = ProcessPoolExecutor(max_workers=paralelo_cpu)
async with aiohttp.ClientSession() as session:
async def processar(url):
async with sem_io:
async with session.get(url) as resp:
bytes_ = await resp.read()
# Filtro CPU-bound em processo separado (escapa GIL)
filtrado = await loop.run_in_executor(
pool, aplicar_filtro, bytes_)
await salvar(filtrado)
async with asyncio.TaskGroup() as tg:
for url in urls:
tg.create_task(processar(url))
pool.shutdown()
ProcessPoolExecutor escapa o GIL fazendo
fork — cada filtro roda em processo separado com Python
próprio. Custo: serialização (pickle) para passar
dados. Em workload onde o tempo de CPU é grande comparado
ao tempo de serialização, ganho é claro. Asyncio
orquestra os I/O.
package main
import (
"context"
"io"
"net/http"
"runtime"
"golang.org/x/sync/errgroup"
)
func processarImagens(ctx context.Context, urls []string) error {
g, ctx := errgroup.WithContext(ctx)
// Semáforo para I/O (limita conexões simultâneas)
semIO := make(chan struct{}, 50)
// Semáforo para CPU (limita filtros simultâneos)
semCPU := make(chan struct{}, runtime.NumCPU())
for _, url := range urls {
url := url
g.Go(func() error {
// Reserva slot I/O
select {
case semIO <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
<-semIO
if err != nil { return err }
defer resp.Body.Close()
bytes, _ := io.ReadAll(resp.Body)
// Reserva slot CPU
select {
case semCPU <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
filtrado := aplicarFiltro(bytes)
<-semCPU
return salvar(ctx, filtrado)
})
}
return g.Wait()
}
Em Go o mesmo modelo de goroutines serve para I/O e CPU —
o runtime distribui em GOMAXPROCS kernel
threads automaticamente. Channels usados como semáforos
limitam concorrência separadamente para I/O (50) e CPU
(= cores). errgroup propaga cancelamento se qualquer
goroutine falha.
Anti-padrões na escolha de modelo
"Mais threads = mais rápido"
Adicionar threads sem entender a natureza do trabalho. Em I/O-bound, depois de um ponto, não ajuda mais (limite é externo). Em CPU-bound, depois do número de cores, prejudica (context switching). Sempre meça throughput vs número de workers.
Async para CPU-bound
Aplicação Node ou asyncio que faz hash de senha (bcrypt) no handler async. O hash bloqueia o event loop por centenas de ms; todas as outras requests param. Solução: mover para worker pool (workers separados em Node, ProcessPoolExecutor em Python). Sintoma: latência tail explodindo enquanto CPU mostra um core em 100%.
Threads para I/O-bound massivo
Servidor Java pré-Loom com pool de 10.000 threads tentando atender 10.000 conexões. 80 GB de stack reservado, scheduler do SO escalando ruim. Em Java moderno (21+), virtual threads resolvem; em outras stacks, async cooperativo já era o caminho certo desde sempre.
multiprocessing onde threading bastaria
Em Python, multiprocessing é caro: fork, pickle entre processos, sem memória compartilhada barata. Para I/O-bound, asyncio ou threading é dramaticamente mais leve. Use multiprocessing apenas quando a escolha é justificada por CPU-bound + GIL.
Distribuir prematuramente
Quebrar em microsserviços por "escalabilidade" antes de esgotar concorrência em uma máquina. Uma máquina moderna (16-128 cores, 64-512 GB de RAM) atende muito mais do que se imagina. Distribuição introduz problemas (latência de rede, falhas parciais, consenso) que não existiam. Distribua quando uma máquina não basta — não antes.
Adotar o modelo da moda em vez do adequado. "Microsserviços" em 2018, "serverless" em 2020, "edge computing" em 2024 — cada época tem padrões que viram resposta automática para qualquer problema, e o resultado é overhead onde não precisa. A pergunta certa não é "qual a tecnologia mais moderna?", é "qual o tipo de trabalho e qual modelo serve a ele?". Sêniores resistem ao mimetismo arquitetural.
A árvore de decisão consolidada
Sumarizando o módulo todo em uma sequência de perguntas:
- O problema é genuinamente concorrente? Se cada passo depende do anterior, escreva síncrono. Se há passos independentes, vale concorrência.
- Tempo é dominado por espera ou por cálculo? Meça com profile ou wall-clock vs CPU-time.
- I/O-bound: escolha modelo cooperativo (asyncio, async/await, goroutines, virtual threads). Dimensione pool/concorrência alto (centenas a milhares).
- CPU-bound: escolha thread/process pool com tamanho = cores físicos. Em Python, considere multiprocessing ou nogil.
- Misto: combine. Modelo cooperativo principal + pool dedicado para fases CPU-bound.
-
Estrutura do problema:
- Fluxo de dados → CSP / channels (Go, Channels em .NET).
- Identidade longa → actor model (Erlang, Akka, Orleans).
- Padrões de pipeline → workers + queues bounded.
- Cancelamento e timeout: propagação end-to-end via contexto. Estrutura léxica (TaskGroup, errgroup) facilita.
- Backpressure: queues bounded em todo lugar. Estratégia explícita quando enchem.
- Estado compartilhado: minimizar. Quando inevitável, primeiro tente imutabilidade ou channels; depois mutex; só então atomics; lock-free só com forte justificativa medida.
- Hardware-aware quando importa: teste em ARM se rodar em ARM (memory model). Considere NUMA em servers grandes. Cache locality em hot paths.
Sumário do módulo — o que você aprendeu
Treze conceitos antes deste, você cobriu: a distinção conceitual fundamental (concorrência vs paralelismo), fundamentos de sistema (threads, processos, scheduler), a teoria por trás (coroutines, fibers), modelos de programação (async/await, structured concurrency, CSP, actors), sincronização (locks, atomics, memory model), e padrões operacionais (workers, pipelines, backpressure, cancelamento). Cada conceito foi um ângulo do mesmo problema central: como múltiplos fluxos cooperam sem se atropelar.
A síntese: concorrência é sobre estrutura, paralelismo é sobre execução; modelos cooperativos são apropriados para I/O massivo e modelos preemptivos para CPU paralelizável; estado compartilhado é a fonte primária de complexidade e deveria ser minimizado; cancelamento, timeout e backpressure são tão importantes quanto o modelo principal de concorrência. Você agora tem vocabulário, ferramentas, e modelos mentais para escolher e implementar com consciência.
O próximo módulo (05) entra em Aspect-Driven & Cross-cutting Concerns: como organizar logging, autenticação, retry, circuit breaker, métricas — coisas que aparecem em todo lugar no código sem caber bem em OOP nem em funcional. Junto com a base de concorrência consolidada aqui, formará a infraestrutura conceitual para discutir resiliência, escalabilidade e operação que vêm nos módulos seguintes.
Como praticar
- Classifique três sistemas que você usa. Pegue três programas/serviços que você toca: classifique cada um como I/O-bound, CPU-bound ou misto. Que modelo usam? Está correto? Se você tivesse que reescrever, manteria? Documente suas conclusões em uma página.
- Profile uma operação ambígua. Pegue uma função em sistema seu sobre a qual você não tem certeza (tipo "esse handler é I/O-bound porque chama banco, mas também faz parsing pesado"). Profile com flame graph (pprof, py-spy, async-profiler). Identifique o gargalo. Documente: qual fração é CPU, qual é wait, qual é I/O. Surpresa é frequente.
- Implemente o mesmo programa em três modelos. Tarefa: 1.000 chamadas HTTP a endpoint público (I/O-bound) + compressão gzip de cada resposta (CPU-bound moderado). Versão A: async puro. Versão B: thread pool puro. Versão C: misto (async para I/O, pool dedicado para CPU). Meça wall clock, memória, CPU usage. Plote os três. Vai descobrir que a versão mista vence em throughput e a versão async pura é a mais econômica em memória — trade-offs em ação.
Referências para aprofundar
- livro Systems Performance — Brendan Gregg (2ª ed., 2020).
- livro The Go Programming Language — Donovan & Kernighan (2015).
- livro Concurrency in C# Cookbook — Stephen Cleary (2ª ed., 2019).
- livro Designing Data-Intensive Applications — Martin Kleppmann (2017).
- artigo Concurrency is not Parallelism — Rob Pike (transcrição, 2012).
- artigo The C10K Problem — Dan Kegel (1999, atualizado).
- artigo The C10M Problem — Robert Graham (2013).
- artigo Go's work-stealing scheduler — Dmitry Vyukov.
- artigo State of the Loom — Ron Pressler (2024).
- docs Site Reliability Engineering — Handling Overload.
- vídeo Choose Boring Technology — Dan McKinley (2015).
- vídeo Three Decades of Concurrency — Doug Lea (Strange Loop).