CodeGym /Cursos /C# SELF /Otimizações com ReaderWrit...

Otimizações com ReaderWriterLockSlim

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

1. Por que o lock comum pode não ser suficiente?

Pensa comigo: temos um recurso compartilhado — por exemplo, uma lista de clientes que mantemos no nosso app multithread. A maior parte do tempo só lemos essa lista — exibimos dados na UI, geramos relatórios, filtramos e ordenamos. Só de vez em quando alguém adiciona ou remove um cliente.

Se a gente usa o lock de sempre, qualquer thread que queira ler tem que esperar até o outro terminar de ler ou (pior) escrever. E se 99% das operações são leitura? A gente acaba empacando leitores desnecessariamente, mesmo sabendo que eles poderiam rodar em paralelo.

É aí que entra um tipo especial de bloqueio — o Reader-Writer Lock. A ideia chave: muitos threads podem ler ao mesmo tempo, mas se alguém precisa modificar, pega acesso exclusivo e bloqueia os leitores.

2. Teoria: o que é ReaderWriterLockSlim

Resumo das classes

  • ReaderWriterLock — mais antigo, mais lento, pode levar a deadlocks. Use só se você precisar dar suporte ao .NET Framework 2.0.
  • ReaderWriterLockSlim — alternativa moderna e rápida (Slim: "magro", "leve"). Essa classe é otimizada para cenários com muitas operações concorrentes de leitura/escrita e é recomendada quando há muitos threads e leituras acontecem muito mais que escritas.

Princípio de funcionamento

ReaderWriterLockSlim implementa três modos de bloqueio:

  • Read Lock (bloqueio de leitura): muitos threads podem ler simultaneamente.
  • Write Lock (bloqueio de escrita): apenas um thread pode modificar os dados, e nenhum outro lê.
  • Upgradeable Read Lock (bloqueio de leitura upgradável): apenas um thread pode manter esse tipo de lock por vez; ele lê, mas se precisar pode "subir" para write lock.

Esquema de bloqueio

Thread-Leitor Thread-Escritor Thread com Upgradeable Read Lock
Leitor ✅/⛔
Escritor
Upgradeable Read ✅*

✅ — permitido

⛔ — bloqueado

* — apenas se o upgradeable read lock não tiver sido "promovido" para write

Quando usar (e quando não usar) ReaderWriterLockSlim

Recomendado:

  • Muitos threads concorrentes que principalmente leem dados compartilhados (referências, cache, configurações).
  • Escritas acontecem raramente (por exemplo, uma vez por segundo/minuto/hora).
  • Importante maximizar performance de leitura sem atrapalhar threads leitores.

Evite:

  • Quando leitura/escrita ocorrem em proporções semelhantes (melhor usar o lock normal).
  • Quando os dados mudam frequentemente (o ganho será pequeno).

3. Usando ReaderWriterLockSlim na prática

Vamos evoluir nosso app de exemplo! Antes a gente montou um diretório simples de clientes e testou acesso multithread à coleção. Agora complica um pouco: temos uma lista de clientes acessada por vários threads (por exemplo, um serviço notifica clientes sobre promoções, outro adiciona novos clientes).

Primeiro, descrevemos nossa coleção de clientes:


// Classe Client — o conhecido personagem das aulas anteriores
public class Client
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Agora vamos criar um wrapper para acesso thread-safe com ReaderWriterLockSlim:

using System;
using System.Collections.Generic;
using System.Threading;

public class ClientDirectory
{
    private readonly List<Client> _clients = new List<Client>();
    // Slim-lock para leitura/escrita
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

    // Adicionar cliente (write lock)
    public void AddClient(Client client)
    {
        _lock.EnterWriteLock(); // Apenas um thread pode adicionar um cliente
        try
        {
            _clients.Add(client);
            Console.WriteLine($"[Thread {Thread.CurrentThread.ManagedThreadId}] Cliente adicionado {client.Name}");
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    // Obter uma cópia da lista de clientes (read lock)
    public List<Client> GetClients()
    {
        _lock.EnterReadLock(); // Muitos threads podem ler ao mesmo tempo
        try
        {
            // Retornamos uma cópia pra ninguém mexer no original por acidente
            return new List<Client>(_clients);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }
}

4. Leitura multithread e escrita periódica

Imagina: 5 threads que periodicamente leem todos os clientes ("geram relatórios" ou só ficam consultando quem tá cadastrado), e outro thread que às vezes adiciona um cliente.

using System.Threading;

class Program
{
    static void Main()
    {
        var directory = new ClientDirectory();

        // Thread que adiciona clientes
        var writerThread = new Thread(() =>
        {
            for (int i = 1; i <= 5; i++)
            {
                directory.AddClient(new Client { Id = i, Name = $"Cliente {i}" });
                Thread.Sleep(700); // Simulação de operações demoradas
            }
        });

        // Vários leitores
        for (int j = 0; j < 5; j++)
        {
            int readerId = j + 1;
            new Thread(() =>
            {
                for (int k = 0; k < 10; k++)
                {
                    var clients = directory.GetClients();
                    Console.WriteLine($"[Leitor {readerId}][Thread {Thread.CurrentThread.ManagedThreadId}] Total de clientes: {clients.Count}");
                    Thread.Sleep(200); // Lemos com mais frequência do que escrevemos
                }
            }).Start();
        }

        writerThread.Start();
        writerThread.Join();

        // Dá um tempo pros leitores terminarem:
        Thread.Sleep(3000);
    }
}

Quase sempre vários threads conseguem ler a lista ao mesmo tempo (sem esperar um pelo outro), mas quando um thread adiciona um cliente — os outros têm que aguardar até a escrita terminar. Isso permite atender muitos leitores sem bloqueios desnecessários.

5. Bloqueio upgradável: UpgradeableReadLock

Às vezes acontece: um thread lê os dados e só às vezes, se uma condição for satisfeita, quer alterá-los. Aparentemente dá pra pegar um read lock, checar a condição, sair do read lock e então pegar o write lock. Mas no tempo "entre" isso outro thread já pode ter mudado os dados! A solução é o upgradeable read lock.

Usar é simples:

public bool AddClientIfNotExists(string name)
{
    _lock.EnterUpgradeableReadLock(); // Apenas um thread pode manter esse tipo de lock
    try
    {
        bool exists = _clients.Exists(c => c.Name == name);
        if (!exists)
        {
            _lock.EnterWriteLock(); // Fazemos o upgrade para write lock
            try
            {
                _clients.Add(new Client { Name = name });
                return true;
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        return false;
    }
    finally
    {
        _lock.ExitUpgradeableReadLock();
    }
}

Primeiro o thread lê, e se precisar mudar — sobe o nível do bloqueio sem liberar o acesso para outros threads no meio do caminho.

6. Dicas úteis

Comparação: ReaderWriterLockSlim vs lock

lock
ReaderWriterLockSlim
Leitura simultânea Não Sim
Escrita simultânea Não Não
Suporta leitura upgradável Não Sim (EnterUpgradeableReadLock)
Velocidade Muito rápido Mais rápido quando há muitas leituras
Memória Mínima Um pouco mais
Simplicidade Fácil Mais complexo, requer atenção

Como isso aparece em projetos reais

Em aplicações grandes, com tabelas enormes ou caches lidos por centenas de threads e raramente atualizados, usar ReaderWriterLockSlim evita gargalos de leitura. Exemplos:

  • Cache de configuração consumido por microserviços.
  • Repositório de dicionários para cálculos financeiros.
  • Cache interno para roteamento de mensagens.

Vale reforçar — na maioria dos casos, especialmente se você está começando com multithreading, o lock tradicional é mais simples e seguro. ReaderWriterLockSlim é pra quando realmente existe muito acesso concorrente de leitura.

Visualização: esquema de acesso

flowchart TD
    A[Leitor 1] --Read--> D[ReaderWriterLockSlim]
    B[Leitor 2] --Read--> D
    C[Escritor] --Write--> D
    D --Read permitido--> E[Leitura da coleção compartilhada]
    D --Write bloqueia leitura/escrita--> F[Escrita na coleção compartilhada]

Enquanto só houver leituras — o acesso é liberado a todos; ao tentar escrever — todos esperam até a escrita terminar.

7. ReaderWriterLockSlim: detalhes e erros comuns

Lock Recursion (Recursão de lock): Por padrão o ReaderWriterLockSlim não permite que o mesmo thread capture repetidamente o mesmo tipo de bloqueio. Ou seja, se um thread já tem um read lock e tentar pegar outro read lock — uma exceção será lançada. O mesmo vale para write lock. No entanto, o UpgradeableReadLock pode ser promovido para write lock de dentro dele.

Não inverta a ordem: Faça EnterUpgradeableReadLock() → dentro dele EnterWriteLock(). Mas nunca o contrário!

Exceções: Se você não liberar o lock (por exemplo, esquecer de chamar ExitWriteLock()), outros threads vão ficar esperando pra sempre. Por isso sempre use try { ... } finally { ... }.

Não segure por muito tempo: Não execute IO longo dentro do bloqueio. Quanto mais tempo o thread segura a trava, mais latência pros outros. Saia do bloco de lock o mais rápido possível.

Avalie a complexidade: Não use ReaderWriterLockSlim pra operações pequeninas onde o lock normal é mais simples e rápido.

2
Tarefa
C# SELF, nível 56, lição 4
Bloqueado
Usando ReaderWriterLockSlim para organizar a leitura
Usando ReaderWriterLockSlim para organizar a leitura
1
Pesquisa/teste
Sincronização de threads, nível 56, lição 4
Indisponível
Sincronização de threads
Problema de recursos compartilhados
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION