MÓDULO 04 · 3 SEMANAS · CONCORRÊNCIA & MODELOS DE EXECUÇÃO

Concorrência & Modelos de Execução

Concorrência é onde sistemas que pareciam simples viram não-determinísticos — e onde a diferença entre "funciona na minha máquina" e "cai sob carga" é decidida.

Duração ~3 semanas
Conceitos 14 fundamentais
Projeto Web crawler concorrente
Pré-requisito Módulos 00, 01, 02 e 03
Performance Confiabilidade Escalabilidade

Quatorze conceitos para construir intuição completa sobre concorrência — dos fundamentos teóricos (vocabulário, scheduler, coroutines) aos padrões de produção (backpressure, cancelamento, escolha de modelo), passando por todos os modelos de programação concorrente que importam hoje. Concorrência mal entendida é a fonte mais comum de bugs caros em produção; quem entende projeta sistemas que envelhecem bem mesmo com tráfego crescendo.

01

Concorrência vs paralelismo: vocabulário e modelos mentais

A distinção que organiza todo o módulo. Dijkstra e Lamport, exemplos concretos de cada um, e por que confundir os dois leva a decisões arquiteturais erradas.

estudar →
02

Threads, processos e o modelo de execução do SO

O que está debaixo da concorrência: scheduler, preempção, context switch. O que o SO te dá de graça e o que ele cobra em cache miss, stack memory e syscalls.

estudar →
03

Coroutines, fibers e execução suspensa

Conway (1963), Knuth, e a ideia teórica que ressurgiu nos anos 2010. Suspensão e retomada — fundamento de async/await, generators e goroutines.

estudar →
04

Async/await e o event loop

Reactor pattern, libuv, single-thread cooperativo. Por que async é "viral" (cor de função) e quando isso é problema.

estudar →
05

Structured concurrency

asyncio.TaskGroup (Python 3.11+), Java Loom (JDK 21), errgroup em Go. Escopos hierárquicos de tarefa e cancelamento que propaga.

estudar →
06

Goroutines e o modelo CSP

Hoare (1978), Pike, e a ideia de comunicar memória em vez de compartilhar. Channels, select, scheduler M:N do runtime do Go.

estudar →
07

Actor model: estado isolado e mensageria

Hewitt (1973), Erlang/OTP, Akka, Orleans (.NET), Pony. Por que "let it crash" é uma filosofia de concorrência, não capricho de Erlang.

estudar →
08

Locks, mutex e condições de corrida

Mutex, RWLock, semáforo, condition variables. Deadlock, livelock, starvation — e race detectors que mostram bug que você juraria não existir.

estudar →
09

Lock-free, atomics e CAS

compare-and-swap, atomic counters, ABA problem. Quando você realmente precisa lock-free (raramente) e quando achou que precisava (sempre).

estudar →
10

Memory models e ordering

Por que threads veem memória diferente. Happens-before, acquire/release, sequential consistency, memory barriers — os bugs mais difíceis de reproduzir nascem aqui.

estudar →
11

Padrões: worker pool, fan-out/fan-in, pipeline

Vocabulário canônico de design concorrente. Pool de workers, distribuição e agregação, pipeline com estágios — abstrações reutilizáveis em qualquer modelo.

estudar →
12

Backpressure e flow control

Reactive Streams, bounded queues, drop strategies, kafka consumer lag. Por que ignorar backpressure é uma das causas mais comuns de outage em sistemas concorrentes.

estudar →
13

Cancelamento, timeout e propagação de contexto

context.Context (Go), CancellationToken (.NET), asyncio.CancelledError (Python). Por que exception-based cancellation é frágil e por que cancelamento explícito venceu.

estudar →
14

Escolhendo o modelo: I/O-bound vs CPU-bound

A pergunta que fecha o módulo: que modelo de concorrência usar para qual problema? Quando cada um ganha, como medir, quando misturar.

estudar →
princípio orientador

Concorrência expõe contratos implícitos. O que parecia uma sequência razoável vira corrida quando há dois fluxos. A maior parte dos bugs sutis em sistemas grandes — perdas, duplicações, deadlocks intermitentes, outages só sob carga — não vem de algoritmo errado, e sim de não articular qual ordem importa, qual operação precisa atomicidade, e quem fala com quem. Tratar concorrência como detalhe de implementação é como tratar segurança como detalhe: o sistema parece funcionar, até falhar de forma cara.

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.

Construir um crawler concorrente força você a sentir cada conceito do módulo em sequência: pool de workers, channels entre fases, cancelamento propagado, backpressure, e a comparação direta com a versão sequencial.

PROJETO PRÁTICO

Web crawler concorrente com worker pool e backpressure

Um crawler que recebe URLs raiz, descobre links filhos até uma profundidade máxima, e armazena resultados em SQLite local. O objetivo não é cobrir o web inteiro — é construir uma máquina concorrente honesta: pool configurável, pipeline em três estágios (fetch, parse, store), backpressure real entre estágios, cancelamento que propaga corretamente, métricas para enxergar a fila, e um comparativo medido com a versão síncrona do mesmo crawler.

REQUISITOS
  • Pool de N workers configurável via flag CLI
  • Pipeline em 3 estágios: fetch → parse → store ligados por filas/channels
  • Backpressure: filas com tamanho limitado; produtor espera quando cheio
  • Cancelamento via context/CancellationToken/asyncio.Event que propaga em todas as fases
  • Limite de taxa por domínio (sem bombardear o mesmo host)
  • Deduplicação — não buscar URL já visitada
  • Métricas: throughput (URLs/s), profundidade da fila por estágio, P50/P95 de latência
  • Run síncrono e concorrente sobre o mesmo input — diff de tempo total documentado
  • Teste de integração com 1000+ URLs locais simulando timeouts e erros 5xx
Performance Confiabilidade
STACK SUGERIDA POR LINGUAGEM
STACK
.NET 10 + HttpClient + Polly + System.Threading.Channels + AngleSharp + EF Core (SQLite) + xUnit + WireMock.Net
ESTRUTURA / NOTAS
  • Crawler.Domain/ (Url, Page, CrawlJob — invariantes)
  • Crawler.Pipeline/ (Fetcher, Parser, Storer — cada um com Channel<T>)
  • Crawler.Infra/ (HttpClient com Polly, EF Core context)
  • Crawler.Cli/ (Program com System.CommandLine)
  • Crawler.Tests/ (xUnit + WireMock para timeouts e 5xx)
  • CancellationToken propagado em todas as awaits
  • Channel<T> bounded com BoundedChannelFullMode.Wait para backpressure
STACK
Python 3.13 + aiohttp + asyncio.TaskGroup + asyncio.Queue (bounded) + lxml + aiosqlite + pytest-asyncio + aioresponses
ESTRUTURA / NOTAS
  • crawler/domain.py (dataclasses imutáveis)
  • crawler/pipeline/ (fetcher.py, parser.py, storer.py)
  • crawler/queues.py (asyncio.Queue com maxsize para backpressure)
  • crawler/cli.py (argparse + asyncio.run)
  • tests/ (pytest-asyncio + aioresponses para simular falhas)
  • asyncio.TaskGroup (3.11+) para structured concurrency
  • Cancelamento via cancel scope da TaskGroup
STACK
Go 1.23 + net/http + chan buffered + sync.WaitGroup + context.Context + golang.org/x/net/html + modernc.org/sqlite + httptest
ESTRUTURA / NOTAS
  • internal/crawler/ (Job, Page — structs)
  • internal/pipeline/ (fetcher.go, parser.go, storer.go — um goroutine cada)
  • internal/queue/ (chan struct{} buffered como semáforo do pool)
  • cmd/crawler/main.go (flag + signal.NotifyContext)
  • internal/.../*_test.go (testing + httptest)
  • context.Context propagado em todas as chamadas que podem bloquear
  • chan T bufferizado define o backpressure entre estágios
entregável

Repositório com README documentando: o gráfico de profundidade da fila e throughput durante um crawl real, o tempo total comparado com a versão sequencial sob o mesmo input, os pontos de cancelamento limpo (e o que acontece se você esquecer um), e um ADR justificando o tamanho de pool escolhido com base em medição. Bonus: um vídeo curto do crawler quebrando quando você remove o backpressure de propósito — entender a falha por dentro vale mais do que ler sobre ela.

Concorrência é tópico recorrente em entrevista de sênior, especialmente para vagas que tocam em sistemas de alto tráfego. As perguntas abaixo cobrem a faixa que diferencia quem usa async de quem entende o que está acontecendo por baixo.

Q.01

Diferença entre concorrência e paralelismo, com um exemplo concreto de cada — e por que importa essa distinção ao decidir uma arquitetura.

Q.02

Explique deadlock. Como detectar em runtime? Quais técnicas em design previnem deadlock antes mesmo do código rodar?

Q.03

Você tem uma função CPU-bound que demora 10s. Como paralelizar em N cores em Go, em C# e em Python? Onde Python tem complicação que as outras não têm?

Q.04

Race condition em incremento de contador entre duas threads — mostre o bug em código, e dê três soluções com trade-offs distintos (mutex, atomic, channel/queue).

Q.05

O que é backpressure? Por que ignorar backpressure é uma das causas mais comuns de outage em sistemas concorrentes — e como você implementaria backpressure num pipeline de três estágios?