Race conditions
Uma race condition ocorre quando o resultado de um programa depende da ordem de execução de threads — e essa ordem é imprevisível. O bug mais clássico: duas threads incrementando o mesmo contador. Parece impossível dar errado, mas counter += 1 não é uma operação atômica.
counter += 1 são três operações, não uma
A linha counter += 1 parece atômica, mas o processador a executa em três
passos: LOAD (lê o valor da memória para um registrador),
ADD (incrementa no registrador) e STORE (escreve de
volta na memória). O sistema operacional pode interromper a thread e escalonar outra
entre qualquer um desses passos.
Se duas threads leem o mesmo valor antigo antes de qualquer uma escrever, ambas incrementam para o mesmo resultado e uma delas sobrescreve a outra. Uma incrementação é perdida.
O que acontece com counter += 1 em duas threads
Compare o cenário intercalado (race condition) com o sequencial correto. Avance passo a passo para ver como o LOAD, ADD e STORE podem se sobrepor.
Por que counter += 1 não é atômico
# O problema: counter += 1 não é atômico # Parece uma instrução, mas são três operações: # 1. LOAD: reg = counter (lê da memória) # 2. ADD: reg = reg + 1 (incrementa) # 3. STORE: counter = reg (escreve na memória) # Se T1 e T2 intercalam essas operações: # T1: LOAD (lê 0) # T2: LOAD (lê 0 — T1 ainda não escreveu!) # T1: ADD, STORE (escreve 1) # T2: ADD, STORE (escreve 1 — sobrescreve T1!) # Resultado: 1 em vez de 2 → dado corrompido import threading counter = 0 def buggy_increment(): global counter for _ in range(500_000): counter += 1 # race condition aqui threads = [threading.Thread(target=buggy_increment) for _ in range(4)] for t in threads: t.start() for t in threads: t.join() print(counter) # esperado: 2.000.000, real: < 2.000.000
Como corrigir: locks e operações atômicas
# Solução 1: Lock (mutex) — exclusão mútua import threading lock = threading.Lock() counter = 0 def safe_increment(): global counter for _ in range(500_000): with lock: # lock()/unlock() automático counter += 1 # agora é atomicamente protegido # Solução 2: operação atômica (mais rápida) import ctypes # C11: _Atomic int counter = 0; # C++11: std::atomic<int> counter{0}; # counter.fetch_add(1) ← instrução única da CPU (LOCK XADD) # Solução 3: variável de thread local (sem compartilhamento) local = threading.local() # Cada thread tem sua própria cópia — sem race condition! # Combina no final com um lock só uma vez. # Heisenbug: race conditions somem com print/debugger # porque a sincronização de I/O muda o timing.
Encontrando e corrigindo race conditions
Mini projeto: reproduza a race condition do contador em Python. Execute com 2 e 4 threads, 1.000 e 1.000.000 iterações por thread. Registre os resultados em uma tabela. Depois corrija com threading.Lock e verifique que o resultado é sempre correto. Quanto mais lenta ficou a versão com lock?
Projeto principal: implemente um banco simplificado: 100 contas com saldo inicial R$1.000 cada. Crie 10 threads que realizam transferências aleatórias entre contas. Sem proteção, o saldo total às vezes difere de R$100.000 — isso é perda de dinheiro simulada! Adicione locks por conta e verifique a invariante: saldo_total sempre = 100.000.
Desafio extra: use a ferramenta ThreadSanitizer (TSan) do compilador GCC/Clang (-fsanitize=thread) para detectar race conditions automaticamente em um programa C. Escreva 3 programas: um com race condition, um correto com mutex, e um com operações atômicas (stdatomic.h). Compare o tempo de execução dos três.
Teste sua intuição
Onde você encontra isso
Therac-25 — race condition fatal
O Therac-25 era uma máquina de radioterapia dos anos 80 que matou 6 pacientes por overdose de radiação. A causa: uma race condition no software de controle. Um operador digitando rapidamente podia entrar num estado de configuração inválida que desativava as proteções de hardware. A lição é ensinada em toda disciplina de engenharia de software crítica.
Corrupção silenciosa em apps
Race conditions em apps iOS/Android causam crashes intermitentes difíceis de reproduzir. Um exemplo clássico: cache de imagens onde duas threads tentam inserir a mesma chave simultaneamente. Em Objective-C, NSMutableDictionary não é thread-safe. A Apple fornece o Instruments Time Profiler e o Thread Sanitizer para detectar esses bugs antes do lançamento.
Bancos de dados — TOCTOU
O ataque TOCTOU (Time Of Check To Time Of Use) é uma race condition de segurança: entre verificar uma permissão e usá-la, o estado pode mudar. Bancos de dados usam transações com isolamento (ACID) para prevenir isso. Sem transação, dois saques simultâneos do mesmo saldo podem resultar em saldo negativo — o "double spend" que o Bitcoin foi projetado para resolver.