1. Introdução
Num aplicativo multithread, um recurso compartilhado é tudo ao que dois ou mais threads podem acessar ao mesmo tempo. Pode ser:
- Uma variável (por exemplo, um contador global ou uma lista).
- Um objeto (por exemplo, uma coleção de usuários).
- Um arquivo ou socket de rede.
- Qualquer estrutura de dados que seja modificada por threads diferentes.
No nosso app de console a gente mais frequentemente tromba com variáveis e objetos que são "sharados" entre threads.
Analogia
Imagine duas pessoas tentando escrever algo no mesmo caderno ao mesmo tempo, sem combinar a vez. Na melhor das hipóteses sai uma escrita torta, na pior — alguém sobrescreve o que o outro escreveu. Em programação é exatamente a mesma coisa, só que esses “pessoas” são threads.
Resumo sobre recursos típicos com race conditions
Na tabela abaixo — os recursos mais comuns que são perigosos para acesso concorrente entre threads:
| Recurso | Tipos de problema | Exemplo |
|---|---|---|
| Variáveis do tipo int | Incremento/decremento incorreto | Contadores, índices |
| Coleções compartilhadas | Perda/corrupção de elementos, exceções | Lista compartilhada de pedidos |
| Objetos | Alterações de estado inconsistentes | Flags, propriedades |
| Arquivos | Corrupção de dados, leitura/escrita incorreta | Arquivos de log, configuração |
2. Race condition: como ela aparece?
Exemplo: contador de visitas
Suponha que a gente quer contar quantas vezes o usuário clicou num botão (ou, no nosso exemplo, quantas vezes threads diferentes incrementaram uma variável). Versão simples do código:
int counter = 0;
void Increment() {
counter++;
}
Agora vamos criar dois threads, em cada um chamando Increment() 100.000 vezes:
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Increment()
{
for (int i = 0; i < 100_000; i++)
{
counter++;
}
}
static void Main()
{
Thread t1 = new Thread(Increment);
Thread t2 = new Thread(Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"Esperado: 200000, obteve-se: {counter}");
}
}
Quantas vezes logicamente o counter deveria ser incrementado? 200000! Mas se você rodar esse código várias vezes, quase certamente verá números diferentes: 185000, 192500, 198765… Por quê?
3. Por que counter++ não é operação atômica?
Como o counter++ funciona de verdade
Em C# e outras linguagens de alto nível o programa vira um conjunto de instruções de máquina. Infelizmente o operador counter++ não se transforma numa única instrução mágica “soma 1 à variável”. O que acontece de verdade:
- O thread LÊ o valor da memória (counter).
- Aumenta esse valor em 1 (num registrador do processador).
- Escreve o novo valor de volta na memória (counter).
Se dois threads fazem isso quase ao mesmo tempo, ambos podem ler o mesmo valor antigo, incrementar e ambos escreverem o mesmo resultado, perdendo um incremento.
Cenário de race
Suponha que counter valia 1000. Ambos os threads leram esse valor (passo 1), ambos incrementaram para 1001 (passo 2), e então ambos escreveram de volta 1001 (passo 3). Que horror: um incremento acabou perdido!
Visualização da race
| Momento no tempo | Thread 1 | Thread 2 | Valor do counter |
|---|---|---|---|
| 1 | Leitura 1000 | 1000 | |
| 2 | Leitura 1000 | 1000 | |
| 3 | Incrementa para 1001 | Incrementa para 1001 | 1000 (ainda sem escrita) |
| 4 | Escreve 1001 | 1001 | |
| 5 | Escreve 1001 | 1001 |
No fim, por dois incrementos o valor só aumentou 1!
4. Mais alguns exemplos: "bugs invisíveis"
E se a race condition não for com números?
Agora imagina que vários threads adicionam elementos na mesma lista:
using System;
using System.Collections.Generic;
using System.Threading;
class Program
{
static List<int> numbers = new List<int>();
static void AddNumbers()
{
for (int i = 0; i < 10000; i++)
{
numbers.Add(i);
}
}
static void Main()
{
Thread t1 = new Thread(AddNumbers);
Thread t2 = new Thread(AddNumbers);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"Esperado: 20000, obteve-se: {numbers.Count}");
}
}
Esse código também pode dar resultados variados a cada execução: às vezes o programa crasha (lança exceção), às vezes você vê menos elementos do que esperava.
Por quê? Porque a coleção List<T> por padrão não é thread-safe. Ou seja, quando dois threads chamam Add ao mesmo tempo, a estrutura interna do list pode ser corrompida.
5. Operações atômicas
O que é operação atômica?
Uma operação atômica é aquela que é executada por inteiro, sem chance de ser interrompida por outro thread no meio. É tipo uma “transação”: ou tudo acontece, ou nada acontece.
- Atribuições de tipo int como myVar = 42; na maioria das plataformas são atômicas (a não ser que seja um objeto gigante).
- Já counter++ não é atômico — são três ações consecutivas.
Operações atômicas especiais
No .NET existem classes especiais para operações atômicas: por exemplo, Interlocked. A gente vai ver esse jeito nas próximas palestras.
Exemplo de incremento atômico usando Interlocked.Increment:
using System.Threading;
int counter = 0;
Interlocked.Increment(ref counter); // operação atômica!
6. Por que é difícil pegar uma race condition?
Race condition é perigosa porque:
- Pode aparecer só sob carga alta.
- É pega não 100% das vezes, mas 5% ou até 0.01% dos casos.
- Cai “aleatoriamente” e acontece onde ninguém espera.
Como reconhecer o problema?
Se a cada execução do programa você obtém resultados diferentes (e errados), vale suspeitar de uma race condition.
Piada de programadores
"Se um bug aparece raramente e some quando você adiciona Thread.Sleep(50) — você tem problemas mais sérios do que imagina."
7. Dicas úteis
Sincronização
Pra proteger seções críticas (trechos de código que mexem com recursos compartilhados), você precisa sincronizar. Mas isso é assunto pras próximas palestras. Por enquanto o principal é aprender a notar e explicar o problema.
Erros típicos de quem tá começando
Muitos iniciantes pensam: “Eu tenho counter++ — o que pode dar errado?” Infelizmente, assim que aparece mais de um thread, qualquer coisa pode dar errado! Mesmo coisas simples: ler/escrever variáveis, adicionar itens numa lista, mudar estado de um objeto e por aí vai.
Onde as races aparecem no mundo real
Em apps multithread modernos (por exemplo, APIs de servidor, processamento de requisições web, jogos e apps móveis) quase sempre existem recursos compartilhados. Sem sincronização, race conditions causam processamento incorreto de pedidos, crashes, vazamentos de memória e uma enorme dor de cabeça no debugging.
Numa entrevista pra vagas de nível middle/senior vão te perguntar: “O que é race condition? Como evitar?” Se você conseguir dar os exemplos acima e explicar a mecânica — os recrutadores vão curtir!
GO TO FULL VERSION