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 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.
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.
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.
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.
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.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} está trabalhando")
} finally {
semaphore.release()
}
}
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.
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.
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.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Dados prontos")
}
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âmetro | Semáforo binário | Semáforo contador |
|---|---|---|
| Intervalo | 0 ou 1 | de 0 a N |
| Threads simultâneas | 1 | até N |
| Aplicação | sinalização, bandeiras | pools de recursos, limitação de velocidade |
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.
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.
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ística | Semaphore | Mutex |
|---|---|---|
| Propriedade | sem proprietário | tem proprietário |
| Liberação | qualquer thread | apenas a thread proprietária |
| Contador | de 0 a N | binário |
| Caso de uso | limitação de paralelismo e sinalização | proteção de seção crítica |
| Recursão | não | sim (reentrante) |
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.
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.
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.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
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.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
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.
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.
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.
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.
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
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.
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.
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.
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.
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
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.
Leia também