Semaphore är en synkroniseringsprimitiv som hanterar åtkomst till en delad resurs via en räknare och en kö av väntande trådar. Enligt Wikipedia, 2024 föreslogs semaforen av Edsger Dijkstra 1965 för att lösa problem med flertrådad interaktion. Verktyget gör det möjligt att begränsa antalet trådar som samtidigt arbetar med en kritisk sektion.
Huvudpunkter
Semaphore — är en synkroniseringsprimitiv som använder en räknare för att hantera åtkomst till en delad resurs. Konceptet föreslogs av Edsger Dijkstra 1965 och blev grunden för alla moderna synkroniseringsmekanismer i operativsystem.
En semafor är en heltalsvariabel med två atomära operationer: wait (acquire) och signal (release). Wait-operationen minskar räknaren och signal ökar den. När räknaren når noll blockeras tråden som anropar wait tills signal utförs av en annan tråd.
Huvudsyftet med semaforen är att skydda kritiska sektioner från samtidig åtkomst av flera trådar. Till skillnad från mutex kräver semaforen ingen bindning till ägartråden, vilket gör den lämplig för ett bredare spektrum av koordinationsuppgifter.
Konceptet med semaforen uppstod i samband med operativsystemet THE, utvecklat vid Technische Hogeschool Eindhoven. Dijkstra formaliserade semaforen som en matematisk abstraktion och bevisade dess tillräcklighet för att implementera vilka synkroniseringsprimitiver som helst.
Semaforens mekanism är baserad på två atomära operationer och en intern väntekö. Vid anrop av acquire kontrollerar tråden räknarens värde och antingen fortsätter exekveringen eller blockeras tills resursen frigörs.
När en semafor skapas ställs startvärdet för tillståndsräknaren in. Varje acquire-anrop minskar räknaren med 1. Om räknaren därefter blir negativ blockeras tråden. Release-operationen ökar räknaren och väcker en av de väntande trådarna.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} arbetar")
} finally {
semaphore.release()
}
}
När en tråd anropar acquire med noll räknare placerar operativsystemet den i semaforens FIFO-kö. Tråden går över till tillståndet BLOCKED och förbrukar ingen processortid. Efter release-anropet går den första tråden i kön över till tillståndet RUNNABLE och får åtkomst till resursen.
I synkroniseringsteorin särskiljs två huvudtyper av semaforer: binär (binary) och räkne (counting). Valet av typ beror på den specifika uppgiften att hantera åtkomst till resurser.
En binär semafor tar endast värdena 0 och 1. I beteendet påminner den om mutex, men utan ägarfordran — vilken tråd som helst kan utföra release. Sådana semaforer är praktiska för att implementera beredskapsflaggor och händelser mellan trådar.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Data är redo")
}
Räknesemafor kan ta vilket icke-negativt värde som helst. Den används för att hantera en pool av homogena resurser där flera instanser är tillgängliga. Till exempel en pool med 5 nätverksanslutningar: varje acquire tar en anslutning, release returnerar den till poolen.
Räknesemaforer är oumbärliga för att begränsa åtkomsthastigheten till externa tjänster och implementera trådpooler. De möjliggör exakt kontroll av graden av parallellism utan manuell trådhantering.
| Parameter | Binär semafor | Räknesemafor |
|---|---|---|
| Intervall | 0 eller 1 | från 0 till N |
| Samtidiga trådar | 1 | upp till N |
| Tillämpning | signalering, flaggor | resurspooler, rate limiting |
Utvecklare blandar ofta ihop semafor och mutex, även om det finns grundläggande skillnader mellan dem. Att förstå dessa skillnader är avgörande för att välja rätt synkroniseringsmekanism i ett projekt.
Den viktigaste skillnaden — ägar konceptet. Mutex vet alltid vilken tråd som har tagit den, och endast den tråden kan frigöra den. Semaforen har ingen ägare: vilken tråd som helst kan anropa release, även utan att anropa acquire. Detta gör mutex säkrare för dataskydd och semaforen mer flexibel för koordination.
I praktiken är mutex snabbare för enkel ömsesidig låsning tack vare optimeringar för det typiska scenariot. Semaforen kräver extra overhead för att underhålla räknaren. För att begränsa parallellism eller implementera mönstret "producent-konsument" är semaforen dock oumbärlig.
| Egenskap | Semaphore | Mutex |
|---|---|---|
| Ägande | ingen ägare | har ägare |
| Frigöring | vilken tråd som helst | endast ägartråd |
| Räknare | från 0 till N | binär |
| Användning | begränsning av parallellism och signalering | skydd av kritisk sektion |
| Rekursion | nej | ja (reentrant) |
I utvecklingen av mobila applikationer används Semaphore för att hantera åtkomst till begränsade resurser: nätverksanslutningar, filer, databaser och hårdvarukomponenter. Moderna plattformar erbjuder praktiska inbyggda implementeringar.
Ett av de typiska fallen — en pool av HTTP-anslutningar. Applikationen kan samtidigt skicka högst 4 förfrågningar till servern, eftersom leverantörens API begränsar parallellism. En semafor med startvärdet 4 garanterar att antalet samtidiga förfrågningar under alla belastningar inte överskrider gränsen och att de återstående trådarna väntar i kön.
Utan semafor, vid en plötslig ökning av användaraktivitet, kan serverinfrastrukturen drabbas av plötslig överbelastning, vilket leder till timeouts och fel 429 Too Many Requests. Semaforen fungerar som en säkring och släpper igenom strikt ett bestämt antal samtidiga anrop oavsett antalet aktiva trådar.
Android tillhandahåller klassen Semaphore från paketet java.util.concurrent. Låt oss titta på ett exempel på att begränsa samtidiga nätverksförfrågningar till två trådar för att förhindra serveröverbelastning.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
I iOS löser DispatchSemaphore från GCD samma uppgift. Utvecklare använder den för att synkronisera åtkomst till resurser i asynkron kod utan att blockera huvudtråden.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
Det vanligaste felet — glömd release vid undantag. Om en tråd avslutas med fel innan den anropar release förblir semaforen för alltid blockerad för de andra trådarna. Använd try/finally eller defer för garanterad frigöring. Det andra problemet är deadlock när flera semaforer tas i olika ordning av olika trådar.
Semaforer används inte bara för dataskydd, utan också för koordination av trådar i komplexa flertrådade scenarier. Kännedom om vanliga mönster påskyndar utvecklingen och minskar sannolikheten för synkroniseringsfel.
Det finns flera beprövade mönster för att tillämpa semaforer i verkliga projekt. Att känna till dem hjälper till att undvika typiska fel och bygga pålitliga flertrådade system.
En semafor med startvärdet N och periodisk release via en timer implementerar hastighetsbegränsning av förfrågningar till API:et. Till exempel tillåter tjänsten 10 förfrågningar per sekund: semaforen startar med 10, varje förfrågan minskar räknaren och en separat TimerTask återställer räknaren till startvärdet en gång per sekund. Detta skyddar både applikationen och servern från överbelastning.
I den klassiska producent-konsument-uppgiften hanterar två semaforer bufferten: empty (skrivtillstånd) och full (lästillstånd). Producenten anropar acquire på empty och release på full, Konsumenten — tvärtom. Detta schema garanterar att Konsumenten aldrig läser en tom buffert och att Producenten inte överfyller den.
Samma schema ligger till grund för den begränsade bufferten i operativsystem — cirkulär buffert med fast storlek. I mobila applikationer används detta mönster för att bearbeta köer av bilder, videofiler och analytiska händelser.
Semaforer används framgångsrikt för throttling av nätverksanrop i bakgrundstjänster. Till exempel skickar en analysapplikation händelsepaket till servern. Utan begränsning av samtidiga trådar vid toppbelastningar (applikationsstart, synkronisering efter offline) kan antalet samtidiga förfrågningar överskrida servergränserna. En semafor med startvärdet 3 garanterar smidig sändning och förhindrar blockering på serversidan.
Vanliga frågor
Semaforen är inte bara en räknare, utan en synkroniseringsprimitiv med atomära operationer och väntekö. En vanlig räknare blockerar inte tråden och garanterar inte atomiciteten av ökning vid samtidig åtkomst av flera trådar.
Ja, deadlock är möjligt när flera semaforer tas i olika ordning av olika trådar. Till exempel tar tråd A S1, sedan S2, och tråd B — S2, sedan S1. Fastställ en enhetlig ordning för att ta alla semaforer i projektet.
Tråden blockeras och går över i vänteläge. Den förbrukar ingen processortid förrän en annan tråd anropar release. I Java är detta tillståndet BLOCKED, i Swift suspenderas tråden av GCD.
Den största skillnaden — ägande. Mutex kan endast frigöras av ägartråden. Binary Semaphore kan frigöras av vilken tråd som helst, vilket är praktiskt för signalering mellan trådar, men mindre säkert för att skydda dataintegritet.
Startvärdet beror på scenariot. För att skydda en enskild resurs — 1. För en pool med N anslutningar — N. För signalering mellan trådar, använd 0 så att konsumenttråden väntar på signal från producenten.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också