CodeGym /Cursos /C# SELF /Problema de recursos compartilhados

Problema de recursos compartilhados

C# SELF
Nível 56 , Lição 0
Disponível

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:

  1. O thread LÊ o valor da memória (counter).
  2. Aumenta esse valor em 1 (num registrador do processador).
  3. 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).
  • 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!

2
Tarefa
C# SELF, nível 56, lição 0
Bloqueado
Detecção de condição de corrida usando um contador inteiro
Detecção de condição de corrida usando um contador inteiro
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION