Semaphore: cos'è, come funziona e il suo utilizzo nella sincronizzazione dei thread

Autore: IT Sectr Pubblicato: 2026-03-19 Tempo di lettura: 8 min

Semaphore è un primitivo di sincronizzazione che controlla l'accesso a una risorsa condivisa attraverso un contatore e una coda di thread in attesa. Secondo Wikipedia, 2024, il semaforo è stato proposto da Edsger Dijkstra nel 1965 per risolvere problemi di interazione multithread. Lo strumento consente di limitare il numero di thread che lavorano contemporaneamente con una sezione critica.

Punti chiave

  • Semaphore è un primitivo di sincronizzazione che controlla l'accesso attraverso un contatore di permessi.
  • Il semaforo binario assume valori 0 e 1, funzionando come una bandiera di blocco.
  • Il semaforo contatore consente l'accesso simultaneo a un numero specificato di thread.
  • A differenza di un mutex, un semaforo non è legato a un thread proprietario.
  • Deadlock è uno dei principali pericoli nell'uso scorretto dei semafori.

Cos'è un Semaphore?

Semaphore è un primitivo di sincronizzazione che utilizza un contatore per controllare l'accesso a una risorsa condivisa. Il concetto è stato proposto da Edsger Dijkstra nel 1965 ed è diventato il fondamento di tutti i meccanismi moderni di sincronizzazione nei sistemi operativi.

Definizione e scopo

Un semaforo è una variabile intera con due operazioni atomiche: wait (acquire) e signal (release). L'operazione wait diminuisce il contatore, mentre signal lo aumenta. Quando il contatore raggiunge lo zero, il thread che chiama wait viene bloccato fino a quando un altro thread esegue signal.

Lo scopo principale di un semaforo è proteggere le sezioni critiche dall'accesso simultaneo di più thread. A differenza di un mutex, un semaforo non richiede legame a un thread proprietario, rendendolo adatto a una gamma più ampia di compiti di coordinamento.

Storia e fondamento teorico

Il concetto di semaforo è nato nel contesto del sistema operativo THE, sviluppato presso la Technische Hogeschool Eindhoven. Dijkstra ha formalizzato il semaforo come astrazione matematica, dimostrandone la sufficienza per implementare qualsiasi primitivo di sincronizzazione.

Come funziona un semaforo?

Il meccanismo del semaforo si basa su due operazioni atomiche e una coda di attesa interna. Quando viene chiamato acquire, il thread controlla il valore del contatore e o continua l'esecuzione o viene bloccato fino al rilascio della risorsa.

Contatore e operazioni atomiche

Quando si crea un semaforo, viene impostato un valore iniziale del contatore di permessi. Ogni chiamata acquire diminuisce il contatore di 1. Se dopo questo il contatore diventa negativo, il thread viene bloccato. L'operazione release aumenta il contatore e risveglia uno dei thread in attesa.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

fun accessResource() {
    semaphore.acquire()
    try {
        println("${Thread.currentThread().name} sta lavorando")
    } finally {
        semaphore.release()
    }
}

Coda di attesa e pianificazione

Quando un thread chiama acquire con un contatore a zero, il sistema operativo lo inserisce nella coda FIFO del semaforo. Il thread passa allo stato BLOCKED, senza consumare tempo CPU. Dopo una chiamata release, il primo thread nella coda passa allo stato RUNNABLE e ottiene l'accesso alla risorsa.

Tipi di semafori

Nella teoria della sincronizzazione, si distinguono due tipi principali di semafori: binario e contatore. La scelta del tipo dipende dal compito specifico di gestione dell'accesso alle risorse.

Semaforo binario (Binary Semaphore)

Un semaforo binario assume solo i valori 0 e 1. Nel comportamento, assomiglia a un mutex, ma senza il requisito di proprietà — qualsiasi thread può eseguire release. Questi semafori sono convenienti per implementare flag di prontezza ed eventi tra thread.

kotlin
val ready = Semaphore(0)

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

fun consumer() {
    ready.acquire()
    println("Dati pronti")
}

Semaforo contatore (Counting Semaphore)

Un semaforo contatore può assumere qualsiasi valore non negativo. Viene utilizzato per gestire un pool di risorse simili dove sono disponibili più istanze. Ad esempio, un pool di 5 connessioni di rete: ogni acquire prende una connessione, release la restituisce al pool.

I semafori contatore sono indispensabili per limitare la velocità di accesso ai servizi esterni e implementare pool di thread. Consentono di controllare con precisione il grado di parallelismo senza gestione manuale dei thread.

ParametroSemaforo binarioSemaforo contatore
Intervallo0 o 1da 0 a N
Thread simultanei1fino a N
Applicazionesegnalazione, flagpool di risorse, limitazione di velocità

Semaphore vs Mutex

Gli sviluppatori spesso confondono semaforo e mutex, sebbene ci siano differenze fondamentali tra loro. Comprendere queste differenze è criticamente importante per scegliere il meccanismo di sincronizzazione corretto in un progetto.

Principio di proprietà

La differenza chiave è il concetto di proprietà. Un mutex sa sempre quale thread lo ha acquisito, e solo quel thread può rilasciarlo. Un semaforo non ha proprietario: qualsiasi thread può chiamare release senza nemmeno aver chiamato acquire. Questo rende il mutex più sicuro per la protezione dei dati e il semaforo più flessibile per il coordinamento.

Prestazioni e casi d'uso

In pratica, un mutex è più veloce per la mutua esclusione semplice grazie alle ottimizzazioni per scenari tipici. Un semaforo richiede overhead aggiuntivo per mantenere il contatore. Tuttavia, per limitare il parallelismo o implementare il modello produttore-consumatore, un semaforo è indispensabile.

CaratteristicaSemaphoreMutex
Proprietànessun proprietarioha un proprietario
Rilascioqualsiasi threadsolo il thread proprietario
Contatoreda 0 a Nbinario
Caso d'usolimitazione del parallelismo e segnalazioneprotezione di sezione critica
Ricorsionenosì (rientrante)

Semaphore nello sviluppo mobile

Nello sviluppo di applicazioni mobili, Semaphore viene utilizzato per gestire l'accesso a risorse limitate: connessioni di rete, file, database e componenti hardware. Le piattaforme moderne forniscono implementazioni integrate convenienti.

Pool di connessioni al server

Un caso d'uso tipico è un pool di connessioni HTTP. Un'applicazione può inviare non più di 4 richieste simultanee a un server perché l'API del fornitore limita il parallelismo. Un semaforo con valore iniziale 4 garantisce che sotto qualsiasi carico, il numero di richieste concorrenti non superi il limite, mentre altri thread aspettano nella coda.

Senza un semaforo, un improvviso aumento dell'attività dell'utente potrebbe causare un sovraccarico improvviso dell'infrastruttura del server, portando a timeout ed errori 429 Too Many Requests. Semaphore agisce come un fusibile, consentendo rigorosamente un numero specificato di chiamate concorrenti indipendentemente dal numero di thread attivi.

Semaphore in Kotlin per Android

Android fornisce la classe Semaphore dal pacchetto java.util.concurrent. Vediamo un esempio di limitazione delle richieste di rete concorrenti a due thread per prevenire il sovraccarico del server.

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

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

DispatchSemaphore in Swift per iOS

In iOS, DispatchSemaphore di GCD risolve lo stesso compito. Gli sviluppatori lo utilizzano per sincronizzare l'accesso alle risorse in codice asincrono senza bloccare il thread principale.

swift
let semaphore = DispatchSemaphore(value: 3)

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

Errori tipici quando si lavora con i semafori

L'errore più comune è un release dimenticato quando si verifica un'eccezione. Se un thread termina con un errore prima di chiamare release, il semaforo rimane permanentemente bloccato per altri thread. Utilizzare try/finally o defer per garantire il rilascio. Il secondo problema è il deadlock quando si acquisiscono più semafori in ordine diverso da thread diversi.

Pattern di utilizzo dei semafori

I semafori sono utilizzati non solo per la protezione dei dati, ma anche per il coordinamento dei thread in scenari multithread complessi. Conoscere i pattern comuni accelera lo sviluppo e riduce la probabilità di errori di sincronizzazione.

Esistono diversi pattern comprovati per l'uso dei semafori in progetti reali. Conoscerli aiuta a evitare errori tipici e costruire sistemi multithread affidabili.

Limitatore di velocità (Rate Limiter)

Un semaforo con valore iniziale N e rilascio periodico tramite timer implementa la limitazione di velocità delle richieste API. Ad esempio, un servizio consente 10 richieste al secondo: il semaforo inizia a 10, ogni richiesta diminuisce il contatore e un TimerTask separato riporta il contatore al valore iniziale ogni secondo. Questo protegge sia l'applicazione che il server dal sovraccarico.

Produttore-Consumatore con semafori

Nel classico problema del produttore-consumatore, due semafori gestiscono un buffer: empty (permessi di scrittura) e full (permessi di lettura). Il produttore chiama acquire su empty e release su full, mentre il consumatore fa l'opposto. Questo schema garantisce che il consumatore non legga mai un buffer vuoto e il produttore non lo trabocchi mai.

Questo stesso schema è alla base del buffer limitato nei sistemi operativi — un buffer circolare di dimensione fissa. Nelle applicazioni mobili, il pattern viene utilizzato per elaborare code di immagini, file video ed eventi analitici.

Throttling delle richieste di rete

I semafori sono utilizzati con successo per il throttling delle chiamate di rete nei servizi in background. Ad esempio, un'applicazione di analisi invia pacchetti di eventi al server. Senza limitare i thread concorrenti durante i picchi di carico (avvio dell'app, sincronizzazione dopo offline), il numero di richieste simultanee può superare i limiti del server. Un semaforo con valore iniziale 3 garantisce un invio fluido e previene il blocco lato server.

Domande frequenti

Qual è la differenza tra un Semaphore e un contatore normale?

Semaphore non è solo un contatore, ma un primitivo di sincronizzazione con operazioni atomiche e una coda di attesa. Un contatore normale non blocca un thread e non garantisce l'atomicità dell'incremento quando più thread accedono concorrentemente.

Un semaforo può causare deadlock?

Sì, il deadlock è possibile quando si acquisiscono più semafori in ordine diverso da thread diversi. Ad esempio, il thread A acquisisce S1, poi S2, mentre il thread B acquisisce S2, poi S1. Fissare un unico ordine di acquisizione per tutti i semafori nel progetto.

Cosa succede quando si chiama acquire con un contatore a zero?

Il thread si blocca e entra in stato di attesa. Non consuma tempo CPU finché un altro thread non chiama release. In Java, questo è lo stato BLOCKED; in Swift, il thread viene sospeso da GCD.

In cosa Binary Semaphore differisce fondamentalmente da Mutex?

La differenza principale è la proprietà. Un Mutex può essere rilasciato solo dal thread proprietario. Un Binary Semaphore può essere rilasciato da qualsiasi thread, il che è conveniente per la segnalazione tra thread ma meno sicuro per proteggere l'integrità dei dati.

Quale valore iniziale scegliere per il contatore?

Il valore iniziale dipende dallo scenario. Per proteggere una singola risorsa — 1. Per un pool di N connessioni — N. Per la segnalazione tra thread, utilizzare 0 in modo che il thread consumatore attenda un segnale dal produttore.

Riepilogo

  • Semaphore è un primitivo di sincronizzazione basato su contatore proposto da Dijkstra nel 1965.
  • Il semaforo binario assume valori 0 e 1; il semaforo contatore assume qualsiasi valore non negativo.
  • Le operazioni acquire e release sono atomiche e thread-safe.
  • A differenza di un mutex, un semaforo non è legato a un thread proprietario.
  • I semafori contatore sono utilizzati per gestire pool di risorse e limitare il parallelismo.
  • Release dimenticato è l'errore più comune che porta al blocco dei thread.

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche