A Semaphore egy szinkronizációs primitív, amely egy számlálón és egy várakozó szálakból álló soron keresztül vezérli a hozzáférést egy megosztott erőforráshoz. A Wikipedia, 2024 szerint a szemaforot Edsger Dijkstra javasolta 1965-ben a többszálú interakció problémáinak megoldására. Az eszköz lehetővé teszi a kritikus szekcióval egyidejűleg dolgozó szálak számának korlátozását.
Főbb pontok
Semaphore — egy szinkronizációs primitív, amely egy számlálót használ a megosztott erőforráshoz való hozzáférés vezérlésére. A koncepciót Edsger Dijkstra javasolta 1965-ben, és az összes modern szinkronizációs mechanizmus alapjává vált az operációs rendszerekben.
A szemafor egy egész típusú változó két atomi művelettel: wait (acquire) és signal (release). A wait művelet csökkenti a számlálót, a signal pedig növeli. Amikor a számláló eléri a nullát, a wait-et meghívó szál blokkolódik, amíg egy másik szál végre nem hajtja a signal-t.
A szemafor fő célja a kritikus szekciók védelme több szál egyidejű hozzáférésétől. A mutex-től eltérően a szemafor nem igényel kötődést a tulajdonos szálhoz, ami szélesebb körű koordinációs feladatokra teszi alkalmassá.
A szemafor koncepciója a THE operációs rendszer kontextusában merült fel, amelyet a Technische Hogeschool Eindhoven-ben fejlesztettek ki. Dijkstra matematikai absztrakcióként formalizálta a szemaforot, bizonyítva annak elegendőségét bármely szinkronizációs primitív megvalósításához.
A szemafor mechanizmusa két atomi műveleten és egy belső várakozási soron alapul. Az acquire hívásakor a szál ellenőrzi a számláló értékét, és vagy folytatja a végrehajtást, vagy blokkolódik az erőforrás felszabadulásáig.
A szemafor létrehozásakor beállításra kerül az engedélyszámláló kezdeti értéke. Minden acquire hívás 1-gyel csökkenti a számlálót. Ha ezután a számláló negatívvá válik, a szál blokkolódik. A release művelet növeli a számlálót és felébreszti az egyik várakozó szálat.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} dolgozik")
} finally {
semaphore.release()
}
}
Amikor egy szál nulla számlálóval hívja meg az acquire-t, az OS elhelyezi a szemafor FIFO sorába. A szál BLOCKED állapotba kerül, nem fogyaszt processzoridőt. A release hívás után a sor első szálja RUNNABLE állapotba kerül és hozzáférést kap az erőforráshoz.
A szinkronizációs elméletben a szemaforok két fő típusát különböztetjük meg: bináris (binary) és számláló (counting). A típus kiválasztása az erőforrásokhoz való hozzáférés kezelésének konkrét feladatától függ.
A bináris szemafor csak 0 és 1 értékeket vesz fel. Viselkedésében a mutex-re hasonlít, de tulajdonjogi követelmény nélkül — bármely szál végrehajthatja a release-t. Az ilyen szemaforok alkalmasak készenléti flag-ek és szálak közötti események megvalósítására.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Az adatok készen állnak")
}
Számláló szemafor bármilyen nem negatív értéket felvehet. Homogén erőforrások pool-jának kezelésére használják, ahol több példány áll rendelkezésre. Például egy 5 hálózati kapcsolatból álló pool: minden acquire elfoglal egy kapcsolatot, a release visszahelyezi a pool-ba.
Számláló szemaforok nélkülözhetetlenek a külső szolgáltatásokhoz való hozzáférés sebességének korlátozásában és a szálpool-ok megvalósításában. Lehetővé teszik a párhuzamosság fokának pontos szabályozását a szálak kézi kezelése nélkül.
| Paraméter | Bináris szemafor | Számláló szemafor |
|---|---|---|
| Tartomány | 0 vagy 1 | 0-tól N-ig |
| Egyidejű szálak | 1 | N-ig |
| Alkalmazás | jelzés, flag-ek | erőforrás pool-ok, rate limiting |
A fejlesztők gyakran összekeverik a szemafort és a mutex-t, bár alapvető különbségek vannak közöttük. E különbségek megértése kritikus fontosságú a megfelelő szinkronizációs mechanizmus kiválasztásához a projektben.
A kulcsfontosságú különbség — a tulajdonjog fogalma. A mutex mindig tudja, melyik szál foglalta le, és csak az a szál szabadíthatja fel. A szemafornak nincs tulajdonosa: bármely szál meghívhatja a release-t, akár az acquire meghívása nélkül is. Ez a mutex-t biztonságosabbá teszi az adatvédelemhez, a szemafor-t pedig rugalmasabbá a koordinációhoz.
A gyakorlatban a mutex gyorsabb az egyszerű kölcsönös zároláshoz a tipikus forgatókönyv optimalizálásainak köszönhetően. A szemafor további overhead-et igényel a számláló fenntartásához. Mindazonáltal a párhuzamosság korlátozásához vagy a “termelő-fogyasztó” minta megvalósításához a szemafor nélkülözhetetlen.
| Jellemző | Semaphore | Mutex |
|---|---|---|
| Tulajdonjog | nincs tulajdonos | van tulajdonos |
| Felszabadítás | bármely szál | csak tulajdonos szál |
| Számláló | 0-tól N-ig | bináris |
| Use case | párhuzamosság korlátozása és jelzés | kritikus szekció védelme |
| Rekurzió | nem | igen (reentrant) |
A mobilalkalmazások fejlesztésében a Semaphore a korlátozott erőforrásokhoz való hozzáférés kezelésére szolgál: hálózati kapcsolatok, fájlok, adatbázisok és hardverkomponensek. A modern platformok kényelmes beépített megvalósításokat kínálnak.
Az egyik tipikus eset — HTTP kapcsolatok pool-ja. Az alkalmazás egyszerre legfeljebb 4 kérést küldhet a kiszolgálónak, mivel a szolgáltató API-ja korlátozza a párhuzamosságot. A 4-es kezdeti értékű szemafor garantálja, hogy bármilyen terhelés mellett az egyidejű kérések száma nem lépi túl a határt, a többi szál pedig a sorban várakozik.
Szemafor nélkül a felhasználói aktivitás hirtelen növekedésekor a kiszolgáló infrastruktúra hirtelen túlterhelődhet, ami időtúllépésekhez és 429 Too Many Requests hibákhoz vezet. A szemafor biztosítékként működik, szigorúan meghatározott számú egyidejű hívást engedélyezve az aktív szálak számától függetlenül.
Az Android a java.util.concurrent csomagból biztosítja a Semaphore osztályt. Vizsgáljuk meg az egyidejű hálózati kérések két szálra korlátozásának példáját a kiszolgáló túlterhelésének megelőzése érdekében.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
iOS-ben a GCD DispatchSemaphore-ja ugyanezt a feladatot oldja meg. A fejlesztők az aszinkron kódban lévő erőforrásokhoz való hozzáférés szinkronizálására használják a főszál blokkolása nélkül.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
A leggyakoribb hiba — az elfelejtett release kivétel esetén. Ha egy szál hiba miatt a release meghívása előtt fejeződik be, a szemafor örökre blokkolva marad a többi szál számára. Használjon try/finally vagy defer-t a garantált felszabadításhoz. A második probléma a deadlock, amikor több szemafor különböző sorrendben történő lefoglalása különböző szálak által történik.
A szemaforokat nem csak adatvédelemre, hanem a szálak koordinálására is használják összetett többszálú forgatókönyvekben. A gyakori minták ismerete felgyorsítja a fejlesztést és csökkenti a szinkronizációs hibák valószínűségét.
Több bevált minta létezik a szemaforok valós projektekben történő alkalmazására. Ezek ismerete segít elkerülni a tipikus hibákat és megbízható többszálú rendszereket építeni.
Az N kezdeti értékű szemafor és egy timeren keresztüli periodikus release megvalósítja az API hívások sebességkorlátozását. Például a szolgáltatás 10 kérést engedélyez másodpercenként: a szemafor 10-zel indul, minden kérés csökkenti a számlálót, és egy külön TimerTask másodpercenként visszaállítja a számlálót a kezdeti értékre. Ez védi az alkalmazást és a kiszolgálót a túlterheléstől.
A klasszikus termelő-fogyasztó feladatban két szemafor kezeli a puffert: empty (írási engedélyek) és full (olvasási engedélyek). A termelő acquire-t hív az empty-n és release-t a full-on, a fogyasztó — fordítva. Ez a séma garantálja, hogy a fogyasztó soha nem olvas üres puffert, és a termelő nem tölti túl azt.
Ugyanez a séma áll a korlátozott puffer mögött az operációs rendszerekben — a körpuffer fix mérettel. Mobil alkalmazásokban ezt a mintát képek, videofájlok és analitikai események sorainak feldolgozására használják.
A szemaforokat sikeresen használják a hálózati hívások throttling-jére háttérszolgáltatásokban. Például egy analitikai alkalmazás eseménycsomagokat küld a kiszolgálónak. A párhuzamos szálak korlátozása nélkül csúcsterheléskor (alkalmazás indítása, offline utáni szinkronizálás) az egyidejű kérések száma meghaladhatja a kiszolgáló korlátait. A 3-as kezdeti értékű szemafor garantálja a zökkenőmentes küldést és megakadályozza a blokkolást a kiszolgáló oldalán.
Gyakran Ismételt Kérdések
A szemafor nem egyszerű számláló, hanem szinkronizációs primitív atomi műveletekkel és várakozási sorral. A közönséges számláló nem blokkolja a szálat és nem garantálja az inkrementáció atomosságát több szál egyidejű hozzáférése esetén.
Igen, a deadlock lehetséges, ha több szemafor különböző sorrendben történő lefoglalása különböző szálak által történik. Például, az A szál lefoglalja S1-et, majd S2-t, a B szál pedig S2-t, majd S1-et. Állítson fel egységes lefoglalási sorrendet a projekt összes szemaforjához.
A szál blokkolódik és várakozó állapotba kerül. Nem fogyaszt processzoridőt, amíg egy másik szál meg nem hívja a release-t. Java-ban ez a BLOCKED állapot, Swift-ben a szálat a GCD felfüggeszti.
A fő különbség — a tulajdonjog. A Mutex csak a tulajdonos szál által szabadítható fel. A Binary Semaphore bármely szál által felszabadítható, ami kényelmes a szálak közötti jelzéshez, de kevésbé biztonságos az adatok integritásának védelméhez.
A kezdeti érték a forgatókönyvtől függ. Egyetlen erőforrás védelméhez — 1. N kapcsolatból álló pool-hoz — N. Szálak közötti jelzéshez használjon 0-t, hogy a fogyasztó szál várjon a termelő jelzésére.
Összefoglalás
Kulcsrakész mobilalkalmazást fejlesztünk
Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.
Olvassa el is