Estágio 01 · 01-11
LockedConcorrência é a fonte mais densa de bugs sutis em software. Race conditions, deadlocks, livelocks, ABA, torn writes, memory reordering, quase ninguém aprende isso direito porque cada linguagem oferece uma abstração diferente e os fundamentos ficam escondidos. Você usa Mutex em Rust, synchronized em Java, lock em C#, Promise em JS, e acha que cada um é um conceito separado. Não é. Embaixo, todo concurrency primitive está implementando uma das poucas regras dos memory models, com trade-offs medidos em ciclos de CPU.
Este módulo é a teoria de baixo de tudo: o que é uma race condition formal, o que é happens-before, o que memory barriers fazem, por que volatile em Java não é o mesmo que volatile em C, como mutexes funcionam por baixo (futex no Linux), o que é lock-free, o que é wait-free, ABA problem, MESI, false sharing, e por que Atomic<T> não basta. Sem isso, 02-07 (Node event loop), 02-09 (Postgres MVCC), 03-11 (Go/Rust concurrency), 04-04 (resilience) viram cargo cult.
Race condition acontece quando o resultado depende da ordem de execução de operações concorrentes que acessam estado compartilhado, e pelo menos uma é escrita.
Não é "às vezes dá errado". É "o resultado não é determinístico em função das entradas". Mesmo que pareça funcionar 1000 vezes, se há ordering ambíguo entre acessos não sincronizados a estado mutável, há race.
Data race (subset mais restrito): dois threads acessam mesma localização, ≥ 1 é write, sem synchronization entre elas. Em C++ e Java, data race em memória não atomic é undefined behavior, não "valor errado", é UB.
CPU moderna não executa na ordem que você escreveu. Faz out-of-order execution, branch prediction, store buffer, cache coherence relaxada. Compiladores também reordenam (instruction scheduling, common subexpression elimination, register allocation).
Para single-thread, isso é invisível: o resultado final é equivalente a sequencial. Para multi-thread, outros threads podem observar reordering. Exemplo clássico (store buffering):
Thread A: x = 1; r1 = y;
Thread B: y = 1; r2 = x;
Em hardware x86 com store buffer, é possível ver r1 == 0 && r2 == 0, porque cada CPU vê suas próprias escritas no store buffer antes de outros CPUs enxergarem. Em ARM/POWER (relaxed memory model), ainda mais reorderings são permitidos.
Memory model define a relação happens-before (HB). Se A happens-before B, então B vê os efeitos de A garantidamente.
HB é construída por:
Se duas operações não estão em relação HB, ordering é indeterminado e race é possível. Toda concorrência correta passa por construir HB explicitamente via primitives.
Barriers são instruções que proíbem reordering através delas:
x86 tem ordering relativamente forte (TSO, Total Store Order); apenas StoreLoad reordering é permitido, e MFENCE resolve. ARM/POWER são fracos; precisa DMB/DSB em mais lugares.
Em C++11+, std::atomic com memory_order_seq_cst (default) emite barriers necessárias; acquire/release/relaxed permitem otimizar. Java volatile desde JSR-133 implica acquire/release. Rust std::sync::atomic espelha C++.
Em multi-core, cada core tem cache. Coerência garante que escritas eventualmente propagam. Protocolo padrão é MESI:
Escrita em linha Shared exige invalidar cópias, custo. Variantes (MOESI, MESIF) otimizam casos. Implicação prática: contenção em cache line, mesmo sem lock, é cara.
Duas variáveis em mesma cache line (~64 bytes em x86), acessadas por threads distintos, geram contenção mesmo sendo independentes. Cada escrita invalida a linha no outro core.
Mitigação: padding (alinhar struct ao tamanho da linha). Em Java, @Contended. Em C++, alignas(std::hardware_destructive_interference_size). Em Go, runtime.CacheLinePadSize.
Código de high-perf concurrency (LMAX Disruptor, scheduler do Go) faz padding obsessivamente.
Mutex é primitive de exclusão mútua. Versão simples: spinlock (test-and-set num atomic). Custa CPU enquanto espera; ok pra critical sections curtas.
Em Linux, mutex de userland sério usa futex (fast userspace mutex):
futex(FUTEX_WAIT) parqueia thread; FUTEX_WAKE desperta.Implicação: lock não-contendido é barato (uns nanosegundos); lock contendido entra em context switch (microssegundos).
pthread_mutex em Linux usa futex. Java synchronized usa locks adaptativos (biased → thin → inflated).
wait (decrement, blocks if 0) e post (increment). Usado pra resource pool, signaling.Trade-off principal: hold time vs custo de context switch. Hold curto → spin. Hold longo → park.
Mars Pathfinder (1997) crashou por priority inversion, um caso famoso.
Exemplo lock-free counter:
loop {
let cur = atomic.load();
if atomic.compare_exchange(cur, cur + 1).is_ok() { break }
}
CAS retorna falha se outro thread escreveu antes; tenta de novo. Em alta contenção, pode ter retry storm. Não é mágica.
CAS verifica que valor é igual ao esperado. Mas valor pode ter mudado de A → B → A entre leitura e CAS, você não detecta. Exemplo: stack lock-free com pointer; node A removido, B colocado e removido, A colocado de volta com lixo. Seu CAS aceita.
Solução: counter de versão (double-word CAS, ou tagged pointer com bits de versão). Hardware com LL/SC (load-linked, store-conditional, em ARM/POWER) detecta naturalmente.
relaxed: só atomicidade, sem ordering. Ex: contador estatístico.acquire: barrier após load. Tudo após o load não pode ser reordenado pra antes.release: barrier antes do store. Tudo antes do store não pode ser reordenado pra depois.acq_rel: ambos (em RMW).seq_cst: ordering total global. Mais forte, mais caro.Padrão release-acquire é o usado pra publicar dados:
// publisher
data = computed_value;
ready.store(true, release);
// consumer
if ready.load(acquire) { use(data) }
Sem release/acquire, consumer pode ler ready=true mas data antigo (CPU reordenou).
Cada modelo é um trade-off em quem garante o quê. Threads delegam tudo ao programador; async restringe interleaving; actors isolam estado; CSP esconde shared memory atrás de canais.
Sim. Não há paralelismo de execução de JS (single thread), mas há concorrência de completions: callbacks rodam interleaved entre await points. Isso gera bugs sutis: você lê state, await, decide com base no que leu, mas outro callback mudou state durante o await.
Workers (Web Workers, Worker Threads em Node) introduzem paralelismo real, com SharedArrayBuffer e Atomics, aí caem todos os memory model issues.
Lock não-contendido custa pouco. Lock contendido custa muito (context switch + cache invalidations). Patterns:
ThreadLocal, Go sync.Pool.Concorrência é design; paralelismo é execução. Programa pode ser concorrente sem ser paralelo (Node) ou paralelo sem ser concorrente (cálculo numérico data-parallel).
Três famílias dominam concorrência aplicada em 2026. Vale entender as diferenças porque escolha errada de modelo destrói código.
CSP (Communicating Sequential Processes), Go, Clojure core.async
Hoare 1978. Processos leves comunicam via canais síncronos. Sem estado compartilhado entre goroutines.
ch := make(chan int)
go func() { ch <- compute() }() // sender bloqueia até alguém receber
result := <-ch // receiver bloqueia até alguém mandar
select: espera múltiplos canais, primitiva de composição.Trade-offs: trivial pra fan-out/fan-in. Difícil pra estado compartilhado complexo (você acaba reinventando mutex via canais). Race detector do Go (go run -race) mitiga, mas não cobre tudo.
Actor model, Erlang, Elixir, Akka (Scala/Java), Pony
Hewitt 1973. Actor é unidade de estado privado que se comunica via mensagens assíncronas num mailbox.
spawn(fn ->
receive do
{:add, x, y, sender} -> send(sender, x + y)
end
end)
Trade-offs: ótimo pra sistemas distribuídos com falhas independentes (WhatsApp em Erlang, Discord em Elixir). Custo: cada interação tem latência de mensagem; sequencing de operações cross-actor exige tracking explícito.
async/await, Rust, JS/TS, C#, Python, Kotlin coroutines
Função suspende (await) sem bloquear thread. Stackless em Rust/C# (compilador transforma em state machine), stackful em algumas runtimes (Java virtual threads, Loom).
async fn fetch_user(id: u64) -> Result<User, Error> {
let resp = client.get(&format!("/u/{id}")).send().await?;
Ok(resp.json().await?)
}
Send.await. Loop quente sem await causa starvation.CancellationToken (.NET), AbortController (JS).Trade-offs: zero-cost em Rust (state machine compilada). Em JS é trivial mas constringido a single-threaded. Java só ganhou virtual threads (Loom, Java 21+), abriu uso de blocking APIs em alta concorrência sem callback hell.
Tabela comparativa
| Aspecto | CSP (Go) | Actors (Erlang) | async/await (Rust/Tokio) |
|---|---|---|---|
| Comunicação | Canal tipado | Mensagem em mailbox | Future + shared state ou channels |
| Isolamento | Convenção | First-class (sem shared) | Convenção (Send/Sync ajuda) |
| Falhas | panic propaga | Crash + supervisor | Result propagado |
| Schedule | Runtime-managed (M:N) | BEAM preempção | Cooperativo, runtime escolhe |
| Estado compartilhado | Possível (mutex) | Não (apenas via msg) | Possível (Arc<Mutex<_>>) |
| Composição | select | Pattern match em receive | join!, select!, tokio::spawn |
| Distribuído | Manual | Native (Erlang distribution) | Manual |
| Hot reload | Não | Native | Não |
Quando escolher:
Modelos não são exclusivos: Akka adiciona streams (CSP-like) sobre actors; Tokio tem tokio::sync::mpsc (channel CSP-style sobre futures).
Você precisa, sem consultar:
r1=0 && r2=0 no exemplo do §2.2.acquire, release, seq_cst. Mostrar quando relaxed basta.volatile em Java (pós-JSR-133) ≠ volatile em C, semântica de memory ordering, não apenas no-cache.Construir uma biblioteca mínima de primitives concorrentes em Rust + benchmark que demonstra ordering issues.
mini_sync:
SpinLock<T> com lock()/unlock() baseado em AtomicBool com acquire/release.Mutex<T> que faz spin curto (até N tentativas) e depois parqueia thread via parking_lot::park ou syscall futex direto (Linux only ok).RwLock<T> com prioridade configurável (writer-preferring vs reader-preferring).Channel<T> bounded lock-free (single-producer single-consumer) usando ring buffer + atomic head/tail com acquire/release.relaxed. Mostre que seq_cst ou release/acquire corrige.criterion mostra diferença.criterion):
std::sync::mpsc em throughput.analysis.md):
std::sync::Mutex ou parking_lot::Mutex no Mutex<T>, implemente.std::sync::atomic e syscalls.loom ou shuttle pra explorar interleavings).loom pra exhaustively model-check seu Channel SPSC.parking_lot::Mutex num bench realista, explique a diferença.loom docs: model checker pra Rust concurrency.