CodeGym /Cursos /C# SELF /Race Condition (estado de corrida)

Race Condition (estado de corrida)

C# SELF
Nível 55 , Lição 4
Disponível

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):

  1. Ler o valor atual de counter (por exemplo, 0)
  2. Incrementar em 1 (dá 1)
  3. 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
Interlocked.Increment
🟩 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.

1
Pesquisa/teste
Introdução à Multithreading, nível 55, lição 4
Indisponível
Introdução à Multithreading
Noções básicas de multithreading em C#
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION