Módulo 8 - Concorrência com threads
Race condition e o Lock
10 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026
O que você vai aprender
- Entender o que é uma race condition e por que ela é imprevisível.
- Reproduzir o bug com duas threads incrementando o mesmo contador.
- Proteger a seção crítica com threading.Lock.
- Usar o Lock como context manager com with.
Ouvir o resumo desta aula
Um recap de cerca de 2 minutos na voz do Valim, para ouvir no trânsito ou na academia.
Ler a transcrição do resumo
Resumo da aula: Race condition e o Lock.
Os objetivos desta aula. Entender o que é uma race condition e por que ela é imprevisível. Reproduzir o bug com duas threads incrementando o mesmo contador. Proteger a seção crítica com threading.Lock. Usar o Lock como context manager com with.
Veja o essencial, parte por parte.
O perigo da memória compartilhada. Threads compartilham memória, então duas podem mexer no mesmo dado ao mesmo tempo.
Reproduzindo o bug. O bug é intermitente: pode passar despercebido em testes e falhar só sob carga real.
O Lock resolve. Prefira sempre with trava a chamar acquire e release na mão; o with libera a trava mesmo se houver exceção.
Esse foi o resumo do essencial. Para se aprofundar, leia a aula completa e responda os exercícios.
O perigo da memória compartilhada
A grande vantagem das threads, compartilhar a memória do processo, é também a maior armadilha. Como todas enxergam as mesmas variáveis, duas threads podem tentar alterar o mesmo dado no mesmo instante, e o resultado depende de qual delas chega primeiro em cada passo. Isso é a race condition, ou condição de corrida: o programa vira uma corrida entre threads pelo dado, e quem vence muda a cada execução. O sintoma clássico é um código que funciona nos testes rápidos e falha de vez em quando na produção, sem um padrão óbvio. A causa quase sempre é uma operação que parece indivisível mas não é.
Pegue o inocente contador += 1. Para o processador, ele são três passos: ler o valor atual do contador, somar um e gravar o resultado de volta. Imagine que o contador vale 10. A thread A lê 10. Antes de ela gravar 11, o sistema passa a vez para a thread B, que também lê 10, soma e grava 11. Agora a thread A volta, grava o seu 11, e as duas somas viraram uma só. Perdemos um incremento. Com milhares de threads e milhões de incrementos, o total final fica bem abaixo do esperado, de forma diferente a cada rodada.
Reproduzindo o bug
Ver o erro acontecer ajuda a nunca mais esquecê-lo. No exemplo abaixo, duas threads incrementam o mesmo contador cem mil vezes cada. O total esperado é duzentos mil, mas rode algumas vezes e você verá números menores e diferentes a cada execução. Não há bug de lógica óbvio; o problema é o acesso concorrente ao mesmo dado sem coordenação. Esse é o tipo de defeito que engana, porque some quando você tenta depurar devagar e volta quando o código roda a toda velocidade.
import threading
contador = 0
def incrementar():
global contador
for _ in range(100_000):
contador += 1 # nao e atomico!
t1 = threading.Thread(target=incrementar)
t2 = threading.Thread(target=incrementar)
t1.start()
t2.start()
t1.join()
t2.join()
print(contador) # esperado 200000, mas costuma sair menosDuas threads no mesmo contador sem trava: o total final vem errado e instável.
O Lock resolve
A solução é garantir que só uma thread por vez execute a seção crítica, o trecho que mexe no dado compartilhado. O threading.Lock faz exatamente isso. Uma thread adquire a trava antes de tocar no contador e a libera depois; enquanto ela segura a trava, qualquer outra que tente adquirir fica esperando. Assim os três passos de ler, somar e gravar acontecem sem interrupção, e nenhum incremento se perde. A forma mais segura de usar o Lock é com o with, que adquire a trava ao entrar no bloco e a libera ao sair, mesmo que ocorra um erro no meio. Você reconhece esse padrão dos context managers.
import threading
contador = 0
trava = threading.Lock()
def incrementar():
global contador
for _ in range(100_000):
with trava: # so uma thread por vez aqui dentro
contador += 1
t1 = threading.Thread(target=incrementar)
t2 = threading.Thread(target=incrementar)
t1.start()
t2.start()
t1.join()
t2.join()
print(contador) # agora sempre 200000with trava garante a seção crítica exclusiva; o total agora é sempre correto.
Teste rápido
Por que contador += 1 pode dar resultado errado com duas threads sem trava?
Perguntas frequentes
- Se o GIL já serializa as threads, por que ainda há race condition?
- Porque o GIL pode trocar de thread entre os passos de uma operação. O contador += 1 vira várias instruções de bytecode, e o interpretador pode passar a vez a outra thread no meio delas. O GIL protege as estruturas internas, mas não torna as suas operações de alto nível atômicas.
- Qual a diferença entre Lock e RLock?
- O Lock comum não pode ser adquirido duas vezes pela mesma thread sem travar. O RLock, ou trava reentrante, permite que a mesma thread o adquira mais de uma vez, liberando na mesma quantidade. Use RLock quando uma função protegida chama outra que também adquire a mesma trava.
- O que é um deadlock?
- É quando duas ou mais threads ficam presas esperando travas que as outras seguram, e nenhuma avança. O caso clássico é a thread A segurar a trava 1 e querer a 2, enquanto a B segura a 2 e quer a 1. Evita-se sempre adquirindo as travas na mesma ordem e travando o mínimo necessário.
- Preciso de Lock para toda variável compartilhada?
- Precisa sempre que várias threads leem e escrevem o mesmo dado e a operação não é atômica. Se um dado é só lido, ou se você usa estruturas próprias para concorrência, como a queue.Queue da próxima aula, a trava manual pode ser desnecessária. A fila já cuida da sincronização internamente.
- Travar demais tem custo?
- Tem. Cada seção crítica só roda em uma thread por vez, então quanto mais código você tranca, mais o programa vira sequencial, perdendo o ganho da concorrência. Proteja o trecho mínimo que toca o recurso compartilhado e faça o trabalho pesado fora da trava sempre que possível.
Fontes
Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.