Sobes.tech
Middle+

Como funciona o trabalho com threads e tarefas em C# quando vários threads acessam um estado comum?

sobes.tech IA

Resposta da IA

Ao trabalhar com um estado compartilhado por vários threads em C#, surgem problemas de sincronização e condições de corrida. Para garantir a segurança dos threads, utilizam-se os seguintes mecanismos:

  1. Bloqueio (Locking):

    • Utiliza-se o operador lock para criar uma secção crítica que garante que apenas um thread possa aceder ao recurso protegido de cada vez.
    • Requer um objeto de bloqueio (geralmente private readonly object) usado para sincronização.
    // Exemplo de uso de lock
    private readonly object _lockObject = new object();
    private int _sharedCounter = 0;
    
    public void IncrementCounter()
    {
        lock (_lockObject)
        {
            _sharedCounter++; // Secção crítica
        }
    }
    
  2. Operações atómicas:

    • A classe System.Threading.Interlocked fornece operações atómicas (por exemplo, Increment, Decrement, Add, Exchange) que são seguras para os threads e não requerem bloqueio explícito para realizar uma operação.
    // Exemplo de uso de Interlocked
    private volatile int _sharedValue = 0; // volatile para visibilidade de alterações
    
    public void UpdateValue(int newValue)
    {
        Interlocked.Exchange(ref _sharedValue, newValue); // Atribuição atómica
    }
    
    public void IncrementValue()
    {
        Interlocked.Increment(ref _sharedValue); // Aumento atómico
    }
    
    • Embora volatile garanta a visibilidade das alterações entre threads, não garante a atomicidade de operações de múltiplos passos. Para operações atómicas, deve-se usar Interlocked.
  3. Classes de sincronização de System.Threading e System.Threading.Tasks:

    • Monitor: mecanismo de bloqueio de baixo nível em comparação com lock.
    • Mutex: permite sincronizar o acesso a um recurso entre vários processos.
    • SemaphoreSlim: limita o número de threads que podem aceder a um recurso simultaneamente.
    • ReaderWriterLockSlim: permite que múltiplos threads leiam um recurso simultaneamente, mas apenas um pode escrever.
  4. Coleções seguras para threads:

    • O espaço de nomes System.Collections.Concurrent contém coleções (ConcurrentBag<T>, ConcurrentDictionary<TKey, TValue>, ConcurrentQueue<T>, ConcurrentStack<T>) projetadas especificamente para acesso concorrente. O uso destas coleções elimina a necessidade de sincronização manual em operações básicas com a coleção.
    // Exemplo de uso de ConcurrentDictionary
    private ConcurrentDictionary<string, int> _sharedData = new ConcurrentDictionary<string, int>();
    
    public void AddOrUpdateData(string key, int value)
    {
        _sharedData.AddOrUpdate(key, value, (k, oldValue) => oldValue + value);
    }
    
  5. Objetos imutáveis:

    • Criar objetos imutáveis significa que, uma vez criados, o seu estado não pode ser alterado. Se o estado compartilhado é representado por um objeto imutável, múltiplos threads podem lê-lo com segurança sem necessidade de sincronização. A atualização do estado consiste em criar uma nova instância do objeto.

Ao trabalhar com tarefas (Task) na TPL (Task Parallel Library), também é necessário considerar a sincronização ao aceder ao estado compartilhado. A TPL facilita a criação e gestão de operações paralelas, mas não elimina a responsabilidade de garantir a segurança dos threads ao trabalhar com dados partilhados.

A escolha do mecanismo de sincronização adequado depende da tarefa específica e das características do acesso ao estado compartilhado.