CodeGym /Cursos /C# SELF /Semeadores: Semaphore

Semeadores: Semaphore e SemaphoreSlim

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

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
Semaphore
Sim Mais lento Quando precisa de sincronização entre processos
SemaphoreSlim
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/SemaphoreSlimN 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.

2
Tarefa
C# SELF, nível 56, lição 3
Bloqueado
Crie um aplicativo de console que simule o funcionamento de um estacionamento com 2 vagas disponíveis.
Crie um aplicativo de console que simule o funcionamento de um estacionamento com 2 vagas disponíveis.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION