Semaphore: o que é, como funciona e sua aplicação na sincronização de threads

Autor: IT Sectr Publicado: 2026-03-19 Tempo de leitura: 8 min

Semaphore é um primitivo de sincronização que controla o acesso a um recurso compartilhado através de um contador e uma fila de threads em espera. De acordo com a Wikipedia, 2024, o semáforo foi proposto por Edsger Dijkstra em 1965 para resolver problemas de interação multithread. A ferramenta permite limitar o número de threads trabalhando simultaneamente com uma seção crítica.

Pontos principais

  • Semaphore é um primitivo de sincronização que controla o acesso através de um contador de permissões.
  • Semáforo binário assume valores 0 e 1, funcionando como uma bandeira de bloqueio.
  • Semáforo contador permite acesso simultâneo a um número especificado de threads.
  • Diferente de um mutex, um semáforo não está vinculado a uma thread proprietária.
  • Deadlock é um dos principais perigos ao usar semáforos incorretamente.

O que é um Semaphore?

Semaphore é um primitivo de sincronização que usa um contador para controlar o acesso a um recurso compartilhado. O conceito foi proposto por Edsger Dijkstra em 1965 e tornou-se a base para todos os mecanismos modernos de sincronização em sistemas operacionais.

Definição e propósito

Um semáforo é uma variável inteira com duas operações atômicas: wait (acquire) e signal (release). A operação wait diminui o contador, enquanto signal o aumenta. Quando o contador chega a zero, a thread que chama wait é bloqueada até que outra thread execute signal.

O principal propósito de um semáforo é proteger seções críticas do acesso simultâneo de múltiplas threads. Diferente de um mutex, um semáforo não requer vínculo a uma thread proprietária, tornando-o adequado para uma gama mais ampla de tarefas de coordenação.

História e fundamento teórico

O conceito do semáforo surgiu no contexto do sistema operacional THE, desenvolvido na Technische Hogeschool Eindhoven. Dijkstra formalizou o semáforo como uma abstração matemática, provando sua suficiência para implementar qualquer primitivo de sincronização.

Como funciona um semáforo?

O mecanismo do semáforo é baseado em duas operações atômicas e uma fila de espera interna. Quando acquire é chamado, a thread verifica o valor do contador e ou continua a execução ou é bloqueada até que o recurso seja liberado.

Contador e operações atômicas

Ao criar um semáforo, um valor inicial do contador de permissões é definido. Cada chamada acquire diminui o contador em 1. Se após isso o contador se tornar negativo, a thread é bloqueada. A operação release aumenta o contador e acorda uma das threads em espera.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

fun accessResource() {
    semaphore.acquire()
    try {
        println("${Thread.currentThread().name} está trabalhando")
    } finally {
        semaphore.release()
    }
}

Fila de espera e escalonamento

Quando uma thread chama acquire com um contador zero, o SO a coloca na fila FIFO do semáforo. A thread transita para o estado BLOCKED, sem consumir tempo de CPU. Após uma chamada release, a primeira thread na fila transita para o estado RUNNABLE e obtém acesso ao recurso.

Tipos de semáforos

Na teoria de sincronização, dois tipos principais de semáforos são distinguidos: binário e contador. A escolha do tipo depende da tarefa específica de gerenciamento de acesso a recursos.

Semáforo binário (Binary Semaphore)

Um semáforo binário assume apenas valores 0 e 1. Em comportamento, assemelha-se a um mutex, mas sem o requisito de propriedade — qualquer thread pode executar release. Esses semáforos são convenientes para implementar bandeiras de prontidão e eventos entre threads.

kotlin
val ready = Semaphore(0)

fun producer() {
    Thread.sleep(1000)
    ready.release()
}

fun consumer() {
    ready.acquire()
    println("Dados prontos")
}

Semáforo contador (Counting Semaphore)

Um semáforo contador pode assumir qualquer valor não negativo. É usado para gerenciar um conjunto de recursos similares onde várias instâncias estão disponíveis. Por exemplo, um pool de 5 conexões de rede: cada acquire toma uma conexão, release a devolve ao pool.

Semáforos contadores são indispensáveis para limitar a velocidade de acesso a serviços externos e implementar pools de threads. Eles permitem controlar precisamente o grau de paralelismo sem gerenciamento manual de threads.

ParâmetroSemáforo binárioSemáforo contador
Intervalo0 ou 1de 0 a N
Threads simultâneas1até N
Aplicaçãosinalização, bandeiraspools de recursos, limitação de velocidade

Semaphore vs Mutex

Desenvolvedores frequentemente confundem semáforo e mutex, embora existam diferenças fundamentais entre eles. Compreender essas diferenças é criticamente importante para escolher o mecanismo de sincronização correto em um projeto.

Princípio de propriedade

A diferença chave é o conceito de propriedade. Um mutex sempre sabe qual thread o adquiriu, e apenas essa thread pode liberá-lo. Um semáforo não tem proprietário: qualquer thread pode chamar release sem sequer ter chamado acquire. Isso torna o mutex mais seguro para proteção de dados e o semáforo mais flexível para coordenação.

Desempenho e casos de uso

Na prática, um mutex é mais rápido para exclusão mútua simples graças a otimizações para cenários típicos. Um semáforo requer sobrecarga adicional para manter o contador. No entanto, para limitar paralelismo ou implementar o padrão produtor-consumidor, um semáforo é indispensável.

CaracterísticaSemaphoreMutex
Propriedadesem proprietáriotem proprietário
Liberaçãoqualquer threadapenas a thread proprietária
Contadorde 0 a Nbinário
Caso de usolimitação de paralelismo e sinalizaçãoproteção de seção crítica
Recursãonãosim (reentrante)

Semaphore no desenvolvimento móvel

No desenvolvimento de aplicações móveis, Semaphore é usado para gerenciar acesso a recursos limitados: conexões de rede, arquivos, bancos de dados e componentes de hardware. Plataformas modernas fornecem implementações integradas convenientes.

Pool de conexões com o servidor

Um caso de uso típico é um pool de conexões HTTP. Uma aplicação pode enviar no máximo 4 requisições simultâneas a um servidor porque a API do provedor limita o paralelismo. Um semáforo com valor inicial 4 garante que sob qualquer carga, o número de requisições concorrentes não exceda o limite, enquanto outras threads esperam na fila.

Sem um semáforo, um aumento brusco na atividade do usuário poderia causar sobrecarga repentina na infraestrutura do servidor, levando a timeouts e erros 429 Too Many Requests. Semaphore age como um fusível, permitindo estritamente um número especificado de chamadas concorrentes independentemente do número de threads ativas.

Semaphore em Kotlin para Android

O Android fornece a classe Semaphore do pacote java.util.concurrent. Vamos ver um exemplo de limitação de requisições de rede concorrentes a duas threads para evitar sobrecarga do servidor.

kotlin
class ApiClient {
    private val throttle = Semaphore(2)

    suspend fun fetch(url: String): Result {
        throttle.acquire()
        return try {
            httpGet(url)
        } finally {
            throttle.release()
        }
    }
}

DispatchSemaphore em Swift para iOS

No iOS, DispatchSemaphore do GCD resolve a mesma tarefa. Desenvolvedores o usam para sincronizar acesso a recursos em código assíncrono sem bloquear a thread principal.

swift
let semaphore = DispatchSemaphore(value: 3)

func processBatch(_ items: [UIImage]) {
    for img in items {
        semaphore.wait()
        DispatchQueue.global().async {
            applyFilter(to: img)
            semaphore.signal()
        }
    }
}

Erros típicos ao trabalhar com semáforos

O erro mais comum é um release esquecido quando ocorre uma exceção. Se uma thread termina com erro antes de chamar release, o semáforo permanece permanentemente bloqueado para outras threads. Use try/finally ou defer para garantir a liberação. O segundo problema é deadlock ao adquirir múltiplos semáforos em ordem diferente por threads diferentes.

Padrões de uso de semáforos

Semáforos são usados não apenas para proteção de dados, mas também para coordenação de threads em cenários multithread complexos. Conhecer padrões comuns acelera o desenvolvimento e reduz a probabilidade de erros de sincronização.

Existem vários padrões comprovados de uso de semáforos em projetos reais. Conhecê-los ajuda a evitar erros típicos e construir sistemas multithread confiáveis.

Limitador de velocidade (Rate Limiter)

Um semáforo com valor inicial N e liberação periódica através de um timer implementa a limitação de velocidade de requisições a uma API. Por exemplo, um serviço permite 10 requisições por segundo: o semáforo começa em 10, cada requisição diminui o contador, e um TimerTask separado retorna o contador ao valor inicial a cada segundo. Isso protege tanto a aplicação quanto o servidor de sobrecarga.

Produtor-Consumidor com semáforos

No problema clássico do produtor-consumidor, dois semáforos gerenciam um buffer: empty (permissões de escrita) e full (permissões de leitura). O produtor chama acquire em empty e release em full, enquanto o consumidor faz o oposto. Este esquema garante que o consumidor nunca leia um buffer vazio e o produtor nunca o transborde.

Este mesmo esquema fundamenta o buffer limitado em sistemas operacionais — um buffer circular de tamanho fixo. Em aplicações móveis, o padrão é usado para processar filas de imagens, arquivos de vídeo e eventos analíticos.

Throttling de requisições de rede

Semáforos são usados com sucesso para throttling de chamadas de rede em serviços em segundo plano. Por exemplo, uma aplicação de análise envia pacotes de eventos ao servidor. Sem limitar threads concorrentes durante picos de carga (inicialização do aplicativo, sincronização após offline), o número de requisições simultâneas pode exceder os limites do servidor. Um semáforo com valor inicial 3 garante envio suave e previne bloqueio do lado do servidor.

Perguntas frequentes

Qual a diferença entre um Semaphore e um contador comum?

Semaphore não é apenas um contador, mas um primitivo de sincronização com operações atômicas e uma fila de espera. Um contador comum não bloqueia uma thread nem garante atomicidade do incremento quando acessado concorrentemente por múltiplas threads.

Um semáforo pode causar deadlock?

Sim, deadlock é possível ao adquirir múltiplos semáforos em ordem diferente por threads diferentes. Por exemplo, a thread A adquire S1, depois S2, enquanto a thread B adquire S2, depois S1. Fixe uma ordem única de aquisição para todos os semáforos no projeto.

O que acontece ao chamar acquire com contador zero?

A thread bloqueia e entra em estado de espera. Ela não consome tempo de CPU até que outra thread chame release. Em Java, este é o estado BLOCKED; em Swift, a thread é suspensa pelo GCD.

Como o Binary Semaphore difere fundamentalmente do Mutex?

A principal diferença é a propriedade. Um Mutex só pode ser liberado pela thread proprietária. Um Binary Semaphore pode ser liberado por qualquer thread, o que é conveniente para sinalização entre threads, mas menos seguro para proteger a integridade dos dados.

Qual valor inicial escolher para o contador?

O valor inicial depende do cenário. Para proteger um único recurso — 1. Para um pool de N conexões — N. Para sinalização entre threads, use 0 para que a thread consumidora espere um sinal do produtor.

Resumo

  • Semaphore é um primitivo de sincronização baseado em contador proposto por Dijkstra em 1965.
  • Semáforo binário assume valores 0 e 1; semáforo contador assume qualquer valor não negativo.
  • As operações acquire e release são atômicas e thread-safe.
  • Diferente de um mutex, um semáforo não está vinculado a uma thread proprietária.
  • Semáforos contadores são usados para gerenciar pools de recursos e limitar paralelismo.
  • Release esquecido é o erro mais comum levando ao travamento de threads.

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também