1. Introdução
Em aplicações multithread, a existência de race condition — é uma questão não de "se", mas de "quando vai acontecer". Mesmo que você ache que seu código é confiável, e que tem só "dois threads pequenos", onde "tudo é óbvio e simples", o estado de corrida pode se esconder no trecho mais inofensivo da lógica.
O que exatamente é um race condition e por que é tão assustador? Imagine duas pessoas tentando editar ao mesmo tempo o mesmo papel — um escreve, o outro apaga. Às vezes tudo bem, às vezes vira algo ilegível. Na programação as consequências são ainda mais engraçadas: erros não aparecem sempre, só em condições específicas, quase aleatórias.
Race condition (estado de corrida) — situação em que o resultado da execução do programa depende de qual thread acessou primeiro o recurso ou executou uma ação. Esse problema ocorre apenas com acesso concorrente (multithread), quando dois ou mais threads acessam dados ou recursos compartilhados.
O que acontece durante a corrida?
Aqui vai um esquema simples. Imagine que temos dois threads e um recurso compartilhado (por exemplo, a variável X):
+---------+ +---------+
| Thread 1| | Thread 2|
+----+----+ +----+----+
| |
| Leitura de X |
| <-------------------|
| |
| Incremento de X |
|-------------------> |
| |
| Escrita de X |
| <-------------------|
Se ambos os threads leem a variável X, incrementam e escrevem de volta ao mesmo tempo, um pode "sobrescrever" a mudança do outro, e o número total de incrementos não vai bater com o esperado.
2. Exemplo clássico de Race Condition
Vamos ver na prática. Suponha que queremos contar cliques de um botão vindos de diferentes threads ou o número de tarefas processadas.
Pegamos uma variável simples e alguns threads que a incrementam:
using System;
using System.Threading;
class Program
{
static int counter = 0; // Recurso compartilhado
static void Main()
{
Thread t1 = new Thread(IncrementCounter);
Thread t2 = new Thread(IncrementCounter);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine("Valor esperado: 200000");
Console.WriteLine("Valor real: " + counter);
}
static void IncrementCounter()
{
for (int i = 0; i < 100_000; i++)
{
counter++; // << É aqui que pode rolar o problema!
}
}
}
O que esperamos?
Como cada thread incrementa o counter 100000 vezes, esperamos que o valor final seja 200000.
O que obtemos na prática?
Às vezes — sim, 200000. Mas na maioria das vezes o valor será menor — às vezes bem menor. Repita o experimento e o resultado vai variar!
Por quê?
A operação counter++ não é atômica. Na verdade ela ocorre assim (simplificado):
- Ler o valor atual de counter (por exemplo, 0)
- Incrementar em 1 (dá 1)
- Escrever de volta (counter = 1)
Se dois threads lerem o valor antigo ao mesmo tempo, ambos podem escrever o novo valor, mas na prática só somaram um incremento.
Visualização com dois threads:
Suponha counter = 0.
- Thread 1: lê 0
- Thread 2: lê 0
- Thread 1: calcula 0 + 1 = 1
- Thread 2: calcula 0 + 1 = 1
- Thread 1: escreve 1
- Thread 2: escreve 1 (perde o incremento do Thread 1)
"Parabéns", você acabou de perder um incremento! Em milhares ou milhões de operações — o resultado vai flutuar bastante.
3. Mais exemplos: não é só o incremento!
Bagunça na cozinha
Pra ficar mais ilustrativo, imagine um café pequeno. Dois cozinheiros fritam omelete na mesma frigideira, mas não coordenam as ações:
- O primeiro põe uma omelete, o segundo já coloca a sua por cima — elas se misturam;
- Um acha que "já coloquei duas omeletes", o outro pensa o mesmo, mas na frigideira tem três em vez de quatro;
- Começa o caos...
Na programação, race condition causa o mesmo "caos": o resultado depende da sequência de operações rápidas e não controladas.
Quando threads se atrapalham: acesso simultâneo a dados
Por exemplo, você implementa um app bancário e um cliente ao mesmo tempo deposita e saca dinheiro da mesma conta usando dois threads (um transfer online, outro no caixa):
account.Balance += 500; // Thread 1: depósito
account.Balance -= 300; // Thread 2: saque
Se essas operações não estiverem protegidas, o saldo final pode ficar incorreto: parte das operações simplesmente "se perde" se os threads rodarem ao mesmo tempo.
4. Nuances úteis
Por que race condition é um problema?
Difícil de capturar e reproduzir. O erro pode aparecer só em máquina sobrecarregada ou em condições raras.
Difícil de debugar. Ao depurar os threads podem se comportar diferente e o erro some.
Integridade dos dados comprometida. Você obtém dados incorretos, corrompidos, às vezes sem notar.
Segurança. Em aplicações críticas race condition pode levar a vazamentos, corrupção de dados e até vulnerabilidades.
Diagrama de "timing da corrida"
+-----------------------+ +-----------------------+
| Thread 1 | | Thread 2 |
+-----------------------+ +-----------------------+
| 1. Ler counter | | |
| 2. Incrementar counter| | |
| (mas não escrever) | | |
| | | 1. Ler counter |
| | | 2. Incrementar counter|
| | | 3. Escrever counter |
| | | (counter = 1) |
| 3. Escrever counter | | |
| (counter = 1) | | |
+-----------------------+ +-----------------------+
Ambos os threads fizeram o incremento, mas no fim só um incremento foi escrito!
Onde o estado de corrida aparece com frequência
- Qualquer variável global ou static acessada por múltiplos threads.
- Listas, filas, coleções que são preenchidas por diferentes threads.
- Eventos e delegates, se subscribe/unsubscribe ocorrerem ao mesmo tempo (por exemplo UI + tarefas em background).
- Cache, dicionários, gerenciamento de conexões.
- Qualquer interação com arquivos, logs, bancos sem transações ou locks.
Como evitar race condition: um pequeno intro
- Sincronização! (mais detalhes nas próximas aulas).
- Use construções da linguagem e libs: lock, Monitor, mutexes, semáforos, etc.
- Para operações simples — métodos atômicos (Interlocked.Increment e afins).
- Use coleções thread-safe (ConcurrentBag, ConcurrentDictionary).
- Sempre pense: "o que acontece se minhas duas funções forem chamadas ao mesmo tempo?"
5. Dicas úteis
Dicas pra encontrar e diagnosticar corridas
- Não confie nem nas operações mais simples (increment ++, atribuição) se usar múltiplos threads.
- Evite acesso compartilhado a variáveis sempre que possível.
- Se vir bugs "flutuantes", difíceis de reproduzir — pense em race conditions!
- Use ferramentas de análise de threads (dotTrace, Concurrency Visualizer, Thread Sanitizer).
- Faça testes de carga — quanto mais threads e operações, maior a chance de achar o erro.
O que é seguro e o que não é sem sincronização
| Operação | Seguro em ambiente multithread? | Explicação |
|---|---|---|
| Atribuição int | 🟩 Às vezes* | Só se um thread escreve e os outros apenas leem, caso contrário — corrida |
| Incremento (++/--) | 🟥 Não | Não é atômico! Race Condition |
| Leitura de string | 🟩 Às vezes* | Se a string não for modificada depois de criada |
| Atribuição de objeto | 🟩 Às vezes* | Contanto que não haja escritas simultâneas |
| Adicionar em List<T> | 🟥 Não | List<T> não é thread-safe |
|
🟩 Sim | Método atômico especial |
— "Às vezes" significa que se apenas um thread escreve e os outros só leem, é seguro; se vários threads podem escrever ao mesmo tempo — sempre há corrida.
6. Erros e armadilhas típicas
No demo acima vimos counter++ como problema. Outra armadilha: incrementar ou checar valor dentro de uma condição.
Exemplo: bug engraçado com "primeira execução"
if (!alreadyStarted)
{
alreadyStarted = true;
// Fazemos a inicialização...
}
Se essa condição for executada por múltiplos threads ao mesmo tempo, cada um pode ver alreadyStarted == false e entrar! Resultado — inicializam algo duas vezes, o que pode causar falha.
GO TO FULL VERSION