Decisões de modelo de concorrência têm vida útil de anos e raramente
são reversíveis sem reescrever boa parte do sistema. Vale articular
cada trade-off antes que a escolha vire restrição estrutural.
Threads tradicionais ou async/await para servidores web?
Async é o default moderno em quase todas as plataformas: C# Task,
Python asyncio, Node.js, Rust tokio — todos foram pensados para o
caso onde a maior parte do tempo é I/O. Threads ganham apenas quando
o trabalho é genuinamente CPU-bound em paralelo, ou quando a stack
de async vira complicada demais para o domínio. Java até pouco
tempo era exceção; com Loom (virtual threads em JDK 21+, 2023), o
modelo async chegou à sintaxe de threads tradicionais. Go ficou de
fora do debate porque goroutines unificam os dois mundos sem o
"cor de função" que torna async viral.
Channels (CSP) ou shared memory + lock?
Não é either-or. Em Go, o ditado "don't communicate by sharing
memory; share memory by communicating" não é absoluto —
sync.Mutex existe e é apropriado para invariantes
internas curtas. Channels são melhores para fluxo (pipeline, worker
pool, fan-out). Locks são melhores para proteger estado interno de
uma struct ou objeto. Em C# e Python, channels são menos
primitivos; usa-se Channel<T>, asyncio.Queue
ou collections concorrentes. Regra prática: estado compartilhado
entre muitos fluxos pede channel ou estrutura concorrente; estado
interno a um objeto pede lock.
Lock-free vale a complexidade?
Lock-free é otimização de gargalo medido. Mutex em Go, C# e Python
é extremamente rápido se a região crítica é curta — dezenas de
nanossegundos sem contenção. Você só precisa de atomics e CAS
quando a região crítica está em hot path com contenção alta:
counter incrementado por todos os threads, hash table compartilhada,
pool de objetos. ABA problem, ordering, memory barriers — tudo isso
é a complexidade que vem junto. Em 99% dos casos: usa mutex e
dorme bem. No 1% restante, mede antes e depois.
Cancelamento explícito ou exception-based?
Cancelamento explícito (context.Context em Go,
CancellationToken em C#, asyncio.CancelledError
em Python) é universalmente o padrão moderno. Exception-based
cancellation foi tentado a sério (Java Thread.interrupt,
.NET ThreadAbort) e quase sempre dá errado: limpeza
de recursos vira frágil, finally blocks ficam em estado indefinido.
Padrão correto: passar token ou contexto por todas as chamadas que
podem demorar, checar regularmente em loops longos, e tratar
cancelamento como caminho normal de execução — não como erro
excepcional.
Quantos workers no pool?
Para CPU-bound, o número de cores físicos é o ponto de partida
(não os lógicos — hyperthreading raramente ajuda em workload
computacional puro e às vezes prejudica por cache trashing).
Para I/O-bound, muito mais alto, limitado pela API externa,
connection pool do banco, ou memória disponível. Nunca chute o
número: meça throughput e latência variando o pool, e fique no
ponto onde adicionar worker para de melhorar latência ou começa a
piorá-la. O ótimo de produção raramente coincide com o "default
razoável" do laboratório.