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
|
|
|
|---|---|---|
| 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.
GO TO FULL VERSION