1. Introdução
Mutex e lock — são como um barista que atende um cliente por vez. Mas e se não tivermos uma única máquina de café, e sim três — e for possível preparar três xícaras ao mesmo tempo?
Por exemplo, você tem uma cafeteria com três máquinas de café. Clientes (threads) chegam, ocupam uma máquina livre, fazem o café e vão embora. Se as três máquinas estiverem ocupadas, os outros esperam até que alguma fique livre.
Pergunta: Como fazer para que no máximo três clientes trabalhem nas máquinas ao mesmo tempo, e os outros esperem na fila?
Resposta: usar um semáforo!
O que é um semáforo?
Semáforo é uma ferramenta clássica de sincronização. Se lock/Mutex controlam "um entra — os outros esperam", o semáforo diz: "permito N ao mesmo tempo!".
Os semáforos foram propostos por Edsger Dijkstra em 1965. O nome veio da sinalização marítima: como bandeiras passavam informações, o semáforo no código avisa às threads — podem entrar ou precisam esperar.
Cenários de uso
- Limitar o número de threads que acessam um recurso ao mesmo tempo.
- Limitar conexões concorrentes ao DB, número de requisições paralelas, tarefas pesadas.
2. Visão geral das classes: Semaphore e SemaphoreSlim
Semaphore
- Classe pesada, usa objetos do kernel do SO (kernel objects).
- Suporta sincronização entre threads de processos diferentes.
- É possível definir um nome e compartilhar entre processos.
SemaphoreSlim
- Versão leve, funciona apenas dentro de um processo.
- Mais rápida e mais econômica em recursos.
- Quase sempre preferível se não precisar de sincronização entre processos.
Analogia: mochila de viagem (SemaphoreSlim) versus mala grande (Semaphore). Viajando leve — leve a mochila.
Tabela comparativa
| Classe | Interprocesso | Performance | Recomendado |
|---|---|---|---|
|
Sim | Mais lento | Quando precisa de sincronização entre processos |
|
Não | Mais rápido | Em 99% dos casos, dentro de um processo |
Principais métodos e propriedades do semáforo
Parâmetros principais
- InitialCount — quantidade inicial de permissões.
- MaxCount — máximo de permissões emitidas ao mesmo tempo.
Métodos chave
- Wait() ou WaitAsync() — requisitar acesso (pegar uma permissão).
- Release() — liberar uma permissão.
Como funciona
Se ao chamar Wait() não houver permissões, a thread é bloqueada e espera até que alguém chame Release(). Após a liberação, uma das threads aguardando continua a execução.
3. Primeiro exemplo prático
Vamos adicionar a um aplicativo de console um "estacionamento" com 3 vagas e tentar rodar 10 threads.
using System;
using System.Threading;
class Program
{
// Semáforo com 3 permissões (3 vagas no estacionamento)
static SemaphoreSlim parking = new SemaphoreSlim(3);
static void Main()
{
for (int i = 1; i <= 10; i++)
{
int carNumber = i;
new Thread(() =>
{
Console.WriteLine($"Carro #{carNumber} está tentando estacionar...");
parking.Wait(); // Espera por uma vaga livre
Console.WriteLine($"Carro #{carNumber} entrou no estacionamento!");
Thread.Sleep(2000); // Fica no estacionamento por 2 segundos
Console.WriteLine($"Carro #{carNumber} está saindo do estacionamento.");
parking.Release(); // Libera a vaga
}).Start();
}
}
}
- Apenas três carros vão "estacionar" ao mesmo tempo.
- Os outros vão esperar pela liberação de uma vaga.
- A saída no console fica misturada — isso é normal em multithreading.
4. Semáforo como limitador de carga
Vamos limitar o número de tarefas pesadas (por exemplo, downloads) para 5 ao mesmo tempo.
static SemaphoreSlim semaphore = new SemaphoreSlim(5); // no máximo 5 downloads concorrentes
static void DownloadFile(int fileId)
{
semaphore.Wait();
try
{
Console.WriteLine($"--> Começando download do arquivo {fileId}");
Thread.Sleep(1000 + fileId * 100); // Baixando (simulação)
Console.WriteLine($"<-- Arquivo {fileId} baixado");
}
finally
{
semaphore.Release();
}
}
static void Main()
{
for (int i = 1; i <= 12; i++)
{
int localId = i;
new Thread(() => DownloadFile(localId)).Start();
}
}
Ponto importante: chame Wait() antes do bloco try, e coloque Release() no finally. Assim a permissão é liberada mesmo se ocorrer uma exceção.
5. Wait(int millisecondsTimeout) e métodos assíncronos
Você pode esperar apenas um tempo limitado:
if (semaphore.Wait(500))
{
// Conseguiu pegar a permissão em meio segundo!
}
else
{
// Não esperamos 500 ms — desistimos
}
Em aplicações modernas (por exemplo, ASP.NET) use a versão assíncrona: await semaphore.WaitAsync(). Isso não bloqueia a thread enquanto espera a permissão.
Observação: em código assíncrono use SemaphoreSlim e seu WaitAsync, caso contrário você pode ter deadlocks inesperados.
6. Exemplos de uso incorreto e correto
Erro comum — esquecer de chamar Release(): as permissões "vazam" e tudo para.
Ruim
static void SomeWork()
{
semaphore.Wait();
// ... processamento, e Release foi esquecido!
}
Bom
static void SomeWork()
{
semaphore.Wait();
try
{
// processamento
}
finally
{
semaphore.Release();
}
}
Versão assíncrona
static async Task SomeAsyncWork()
{
await semaphore.WaitAsync();
try
{
// processamento assíncrono
}
finally
{
semaphore.Release();
}
}
7. Funcionamento interno do semáforo (explicação "na prática")
Semáforo é um contador. Wait() decrementa ele em 1. Se ele for > 0 — a thread passa; se for 0 — a thread espera. Release() incrementa o contador e acorda os aguardando.
+-------------------------------+
| Semáforo (contador = 3) |
+-------------------------------+
| [ ] [ ] [ ] | <--- Permissões
+----+----+----+----------------+
| | |
Thread Thread Thread
8. Nuances úteis
Diferença para outros primitivos
- lock / Monitor / Mutex — permitem apenas uma thread (acesso exclusivo).
- Semaphore/SemaphoreSlim — permitem até N threads simultâneas.
O semáforo não está ligado a um "dono": qualquer thread pode liberar a permissão. Isso é uma característica, não um bug.
Aplicações na vida real
- Limitar conexões paralelas a um serviço ou DB.
- Pools: no máximo N threads por recurso.
- Limitar requisições web processadas simultaneamente.
- Limitar leitura/gravação para evitar sobrecarga.
- Limitar chamadas a APIs externas.
Exemplo de erro (Release mais que Wait)
var semaphore = new SemaphoreSlim(2);
semaphore.Release(); // Erro! O contador virou 3, ultrapassando MaxCount — será lançado SemaphoreFullException.
Aqui haverá SemaphoreFullException: o contador excedeu o máximo.
Diferenças entre Semaphore e SemaphoreSlim
- SemaphoreSlim — intra-processo, mais rápido e simples (use quase sempre).
- Semaphore — necessário para sincronização entre processos (cenário raro).
Por que aprender sobre semáforos?
Pergunta clássica em entrevistas: "Como limitar o número de threads trabalhando com um recurso?" — a resposta correta: semáforo.
- lock — 1 thread.
- Semaphore/SemaphoreSlim — N threads.
9. Erros típicos e peculiaridades no uso de semáforos
Erro nº1: esquecem de chamar Release(). Se uma thread pegou a permissão (Wait() ou WaitAsync()) mas não a liberou, os outros vão esperar pra sempre — a aplicação "congela".
Erro nº2: chamam Release() mais vezes do que houve Wait(). Surgem permissões "extras". Para Semaphore isso vai causar SemaphoreFullException e lógica de acesso quebrada.
Erro nº3: misturam diferentes mecanismos de sincronização. Em um lugar — lock, em outro — semáforo para o mesmo recurso. Isso aumenta o risco de deadlocks.
Erro nº4: usam Semaphore em código assíncrono. O semáforo clássico não é amigo de async/await. Para cenários assíncronos use SemaphoreSlim e WaitAsync().
Erro nº5: configuram errado initialCount e maxCount. Com valores incorretos a restrição pode ser contornada, e mais threads do que o planejado acessarão o recurso.
GO TO FULL VERSION