Semaphore is een synchronisatieprimitief die de toegang tot een gedeelde bron beheert via een teller en een wachtrij van wachtende threads. Volgens Wikipedia, 2024 werd de semafoor in 1965 voorgesteld door Edsger Dijkstra voor het oplossen van problemen met multithread-interactie. Het hulpmiddel maakt het mogelijk het aantal threads dat gelijktijdig met een kritieke sectie werkt te beperken.
Belangrijkste punten
Semaphore — is een synchronisatieprimitief die een teller gebruikt om toegang tot een gedeelde bron te beheren. Het concept werd in 1965 voorgesteld door Edsger Dijkstra en werd de basis van alle moderne synchronisatiemechanismen in besturingssystemen.
Een semafoor is een integer variabele met twee atomaire bewerkingen: wait (acquire) en signal (release). De wait-bewerking verlaagt de teller en signal verhoogt deze. Wanneer de teller nul bereikt, wordt de thread die wait aanroept geblokkeerd totdat signal door een andere thread wordt uitgevoerd.
Het belangrijkste doel van een semafoor is het beschermen van kritieke secties tegen gelijktijdige toegang door meerdere threads. In tegenstelling tot mutex vereist een semafoor geen binding aan de eigenaarthread, waardoor het geschikt is voor een breder scala aan coördinatietaken.
Het concept van de semafoor ontstond in de context van het besturingssysteem THE, ontwikkeld aan de Technische Hogeschool Eindhoven. Dijkstra formaliseerde de semafoor als een wiskundige abstractie en bewees dat deze voldoende is voor het implementeren van alle synchronisatieprimitieven.
Het mechanisme van de semafoor is gebaseerd op twee atomaire bewerkingen en een interne wachtrij. Bij het aanroepen van acquire controleert de thread de tellerwaarde en gaat ofwel verder met uitvoeren, of wordt geblokkeerd totdat de bron vrijkomt.
Bij het maken van een semafoor wordt de beginwaarde van de vergunningenteller ingesteld. Elke acquire-aanroep verlaagt de teller met 1. Als de teller daarna negatief wordt, wordt de thread geblokkeerd. De release-bewerking verhoogt de teller en wekt een van de wachtende threads.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} werkt")
} finally {
semaphore.release()
}
}
Wanneer een thread acquire aanroept met een teller van nul, plaatst het besturingssysteem deze in de FIFO-wachtrij van de semafoor. De thread gaat naar de status BLOCKED en verbruikt geen processortijd. Na de release-aanroep gaat de eerste thread in de wachtrij naar de status RUNNABLE en krijgt toegang tot de bron.
In de synchronisatietheorie worden twee hoofdtypen semaforen onderscheiden: binair (binary) en telbaar (counting). De keuze van het type hangt af van de specifieke taak van toegangsbeheer tot bronnen.
Een binaire semafoor neemt alleen de waarden 0 en 1 aan. Qua gedrag lijkt het op een mutex, maar zonder de eigendomsvereiste — elke thread kan release uitvoeren. Dergelijke semaforen zijn handig voor het implementeren van gereedheidsvlaggen en gebeurtenissen tussen threads.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Gegevens zijn gereed")
}
Telbare semafoor kan elke niet-negatieve waarde aannemen. Het wordt gebruikt voor het beheren van een pool van gelijksoortige bronnen waar meerdere exemplaren beschikbaar zijn. Bijvoorbeeld een pool van 5 netwerkverbindingen: elke acquire neemt een verbinding in beslag, release retourneert deze naar de pool.
Telbare semaforen zijn onmisbaar bij het beperken van de toegangssnelheid tot externe diensten en het implementeren van threadpools. Ze maken nauwkeurige controle van de mate van parallellisme mogelijk zonder handmatig threadbeheer.
| Parameter | Binaire semafoor | Telbare semafoor |
|---|---|---|
| Bereik | 0 of 1 | van 0 tot N |
| Gelijktijdige threads | 1 | tot N |
| Toepassing | signalering, vlaggen | bronnenpools, rate limiting |
Ontwikkelaars verwarren vaak semafoor en mutex, hoewel er fundamentele verschillen tussen beide zijn. Het begrijpen van deze verschillen is cruciaal voor het kiezen van het juiste synchronisatiemechanisme in een project.
Het belangrijkste verschil — het concept van eigendom. Mutex weet altijd welke thread hem heeft ingenomen en alleen die thread kan hem vrijgeven. Een semafoor heeft geen eigenaar: elke thread kan release aanroepen, zelfs zonder acquire aan te roepen. Dit maakt mutex veiliger voor gegevensbescherming en semafoor flexibeler voor coördinatie.
In de praktijk is mutex sneller voor eenvoudige wederzijdse vergrendeling dankzij optimalisaties voor het typische scenario. Een semafoor vereist extra overhead voor het onderhouden van de teller. Voor het beperken van parallellisme of het implementeren van het patroon “producent-consument” is een semafoor echter onmisbaar.
| Kenmerk | Semaphore | Mutex |
|---|---|---|
| Eigendom | geen eigenaar | heeft eigenaar |
| Vrijgave | elke thread | alleen eigenaarthread |
| Teller | van 0 tot N | binair |
| Use case | parallellisme beperken en signalering | bescherming kritieke sectie |
| Recursie | nee | ja (reentrant) |
Bij de ontwikkeling van mobiele applicaties wordt Semaphore gebruikt voor het beheren van toegang tot beperkte bronnen: netwerkverbindingen, bestanden, databases en hardwarecomponenten. Moderne platforms bieden handige ingebouwde implementaties.
Een van de typische gevallen — een pool van HTTP-verbindingen. De applicatie kan maximaal 4 gelijktijdige verzoeken naar de server sturen, omdat de API van de provider parallellisme beperkt. Een semafoor met beginwaarde 4 garandeert dat bij elke belasting het aantal gelijktijdige verzoeken de limiet niet overschrijdt en de overige threads in de wachtrij wachten.
Zonder semafoor kan bij een plotselinge toename van gebruikersactiviteit de serverinfrastructuur overbelast raken, wat leidt tot time-outs en fouten 429 Too Many Requests. Semafoor werkt als een zekering die strikt een bepaald aantal gelijktijdige aanroepen doorlaat, ongeacht het aantal actieve threads.
Android biedt de klasse Semaphore uit het pakket java.util.concurrent. Laten we een voorbeeld bekijken van het beperken van gelijktijdige netwerkverzoeken tot twee threads om overbelasting van de server te voorkomen.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
In iOS lost DispatchSemaphore van GCD dezelfde taak op. Ontwikkelaars gebruiken het voor het synchroniseren van toegang tot bronnen in asynchrone code zonder de hoofdthread te blokkeren.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
De meest voorkomende fout — vergeten release bij een uitzondering. Als een thread met een fout eindigt voordat release wordt aangeroepen, blijft de semafoor voor altijd geblokkeerd voor de andere threads. Gebruik try/finally of defer voor gegarandeerde vrijgave. Het tweede probleem is deadlock bij het innemen van meerdere semaforen in verschillende volgorde door verschillende threads.
Semaforen worden niet alleen gebruikt voor gegevensbescherming, maar ook voor coördinatie van threads in complexe multithread-scenario’s. Kennis van veelvoorkomende patronen versnelt de ontwikkeling en vermindert de kans op synchronisatiefouten.
Er zijn verschillende bewezen patronen voor het toepassen van semaforen in echte projecten. Het kennen ervan helpt typische fouten te voorkomen en betrouwbare multithread-systemen te bouwen.
Een semafoor met beginwaarde N en periodieke release via een timer implementeert snelheidsbeperking van verzoeken naar de API. Bijvoorbeeld, de dienst staat 10 verzoeken per seconde toe: de semafoor start met 10, elk verzoek verlaagt de teller en een aparte TimerTask herstelt eenmaal per seconde de teller naar de beginwaarde. Dit beschermt zowel de applicatie als de server tegen overbelasting.
In de klassieke producent-consument taak beheren twee semaforen de buffer: empty (schrijfvergunningen) en full (leesvergunningen). De producent roept acquire aan op empty en release op full, de consument — omgekeerd. Dit schema garandeert dat de consument nooit een lege buffer leest en de producent deze niet overvult.
Hetzelfde schema vormt de basis van de begrensde buffer in besturingssystemen — de ringbuffer met vaste grootte. In mobiele applicaties wordt dit patroon gebruikt voor het verwerken van wachtrijen met afbeeldingen, videobestanden en analytische gebeurtenissen.
Semaforen worden succesvol gebruikt voor throttling van netwerkoproepen in achtergronddiensten. Bijvoorbeeld, een analytische applicatie stuurt gebeurtenispakketten naar de server. Zonder beperking van gelijktijdige threads kan bij piekbelastingen (opstarten van de applicatie, synchronisatie na offline) het aantal gelijktijdige verzoeken de serverlimieten overschrijden. Een semafoor met beginwaarde 3 garandeert soepel verzenden en voorkomt blokkering aan de serverzijde.
Veelgestelde vragen
Semafoor is niet zomaar een teller, maar een synchronisatieprimitief met atomaire bewerkingen en een wachtrij. Een gewone teller blokkeert de thread niet en garandeert geen atomiciteit van de incrementatie bij gelijktijdige toegang van meerdere threads.
Ja, deadlock is mogelijk bij het innemen van meerdere semaforen in verschillende volgorde door verschillende threads. Bijvoorbeeld, thread A neemt S1 in, dan S2, en thread B — S2, dan S1. Stel een uniforme volgorde van innemen vast voor alle semaforen in het project.
De thread wordt geblokkeerd en gaat in de wachtstand. Hij verbruikt geen processortijd totdat een andere thread release aanroept. In Java is dit de status BLOCKED, in Swift wordt de thread door GCD onderbroken.
Het belangrijkste verschil — eigendom. Mutex kan alleen worden vrijgegeven door de eigenaarthread. Binary Semaphore kan door elke thread worden vrijgegeven, wat handig is voor signalering tussen threads, maar minder veilig voor het beschermen van gegevensintegriteit.
De beginwaarde hangt af van het scenario. Voor het beschermen van een enkele bron — 1. Voor een pool van N verbindingen — N. Voor signalering tussen threads gebruik 0, zodat de consumententhread wacht op een signaal van de producent.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook