Szemaforok
A fejezet tartalma:A szemafor
A szemafor olyan absztrakt adattípus, amit az osztott erőforrások egy készletéhez való hozzáférés szabályozásához, illetve programszálak szinkronizálásához használnak a többszálú környezetekben. Megalkotása Edsger Dijkstra holland matematikusnak, a programozás egyik úttörőjének nevéhez fűződik.
1. ábra: A szemafor, mint az osztott erőforrások egy készletéhez való hozzáférés szabályozója
A programozási szakirodalomban többnyire külön említik a bináris szemaforokat (amelyek értéke csak 1 vagy 0 lehet), s ezeket tévesen a mutexekkel azonosítják. Eezktől megkülönböztetve említik a számláló szemaforokat, melyek értéke 1-nél nagyobb is lehet, s ugyancsak tévesen a mutexek általánosításának tekintik. Michaell Barr írása (Mutexes and Semaphores Demystified) megemlíti, hogy a mutexek és szemaforok fogalmának összekeverése történelmi eredetű, s egyszerű analógia segítségével tisztázza, hogy mi a különbség köztük:
- A mutex egy olyan kulcs, amelyet az operációs rendszer ad, hogy a programszál zárolni tudja egy erőforráshoz való hozzáférést a programfutás egy kritikus szakaszán. Ahhoz hasonlítható, mint a benzinkútnál a mosdó kulcsa, amelyből csak egy van. Aki a kulcsot megkapja, használhatja a mosdót, majd visszaszolgáltatja a kulcsot. Ha a kulcs éppen ki van adva, a többi vendég sorban áll a kulcsért. Ehhez hasonlóan a mutex használata is arra szolgál, hogy sorbarendezze a konkurens hozzáférési igényeket. Ha csak ennyi volna a mutex, akkor valóban azonosíthatnánk a bináris szemaforral, amelynek 1 értéke az erőforrás szabad, 0 értéke pedig a foglalt állapotnak fele meg. Ám a mutex ennél többet jelent, hiszen implementációjában az operációs rendszer kezeli a programszálaknál tárgyal prioritás inverzió problémáját is.
- A fenti, egyszerű erőforrás hozzáférésvédelmi protokol azonban nem skálázható két egyforma erőforrás esetére a számláló szemaforok bevezetésével, ahogy azt sokan tévesen gondolják. A fenti analógia alapján például tegyük fel, hogy két egyforma mosdónk van, két egyforma kulccsal. A számláló szemafor pedig azt jelzi, hogy ezek közül hány kulcs van még szabadon. Ha azonban a vendég megkapja az egyik kulcsot, még nem tudja, hogy melyik mosdó szabad. A szemafor tehát önmagában nem oldja meg a több azonos erőforrás megosztásának problémáját, ehhez további, kiegészítő információra is szükség van!
- A szemaforok rendeltetésszerű felhasználása többnyire inkább arra szolgál, hogy segítségével egyik programszál jelezzen a másiknak. Míg a(1: vörös, 2: zöld, 3: kék) mutexet ugyanaz a programszál szabadítja fel, amelyik korábban zárolta, a szemaforok használatánál többnyire az egyik szál (a producer = termelő) növeli a szemafor értékét (ami pl. jelezheti azt, hogy hány adat áll rendelkezésre egy FIFO tárban), a másik programszál (a consumer = fogyasztó) pedig felhasználja a FIFO tárban elhelyezett adatokat és mindig csökkenti a szemafor értékét.
- A szemafor arra is szolgálhat,
hogy egy megszakítást kiszolgáló eljárás (ISR) jelezzen valamelyik
programszálnak, hogy bekövetkezett a várt esemény (pl. az ADC végzett egy méréssel, vagy a DMA átvitte az előírt adatblokkot).
A Semaphore objektumosztály tagfüggvényei
Az osztott erőforrások egy készletéhez való hozzáférést szabályozó szemaforok létrehozása és kezelése a Semaphore objektumosztály segítségével végezhető, tagfüggvényeit az alábbi táblázatban foglaltuk össze. Az mbed-rtos-ban implementált Semaphore objektumosztály ún. számláló szemaforokat kezel, amelyek értékének nincs felső határa (csak a számábrázolásból eredő int32_t típushoz tartozó fizikai korlát).Függvénynév |
Funkció |
---|---|
Semaphore
név(szám) |
Létrehoz egy "név" nevű szemafor
objektumot és inicializálja a megadott számmal (a szám a kezdetben
rendelkezésre álló erőforrások száma). |
wait(time) |
Várakozik, amíg a szemaforral
kezelt erőforrások valamelyike elérhetővé válik, majd lefoglalja
(eggyel csökkenti a szemafor értékét). Ha a szemafor nulla (nincs
elérhető erőforrás), a várakozás alapértelmezetten korlátlan ideig
tarthat, vagy a time
paraméterben ezredmásodpercekben megadott idő leteltekor időtúllépéssel
kilép a várakozásból. |
release() |
Felszabadítja (eggyel növeli a
szemafor értékét) a korábban wait() metódussal
lefoglalt szemafort. |
Mintapélda: Étkező
filozófusok - a könnyű út
Az előző fejezetben ismertetett "étkező filozófusok" problémján az
alábbi
programban egy egyszerűsítő könnyítést alkalmazunk: a filozófusok nem
csak a tányérjuk melletti pálcikákat vehetik
fel, hanem bármelyik két szabad pálcikát! Mivel így nem kell nyilván
tartanunk, hogy melyik erőforrás szabad, mutexek helyett elég lesz egy
szemafor is, amelyben nyilvántartjuk, hogy hány erőforrás van még
szabadon.Hardver követelmények:
- FRDM-KL25z kártya
#include "mbed.h"
#include "rtos.h"
Semaphore s(5); //a pool of 5 chopsticks
Mutex stdio_mutex; //Mutex required for Cortex-M0
Timer mytime;
void notify(const char* name) {
stdio_mutex.lock();
printf("%s acquired two chopsticks %8.1f\n\r", name,mytime.read());
stdio_mutex.unlock();
}
void test_thread(void const* args) {
while (true) {
Thread::wait(1000+rand()%500); //Thinking
s.wait();
s.wait();
notify((const char*)args);
Thread::wait(500+rand()%500); //Eating
s.release();
s.release();
}
}
int main (void) {
mytime.start();
Thread t2(test_thread, (void *)"Philosopher 2");
Thread t3(test_thread, (void *)"Philosopher 3");
Thread t4(test_thread, (void *)"Philosopher 4");
Thread t5(test_thread, (void *)"Philosopher 5");
test_thread((void *)"Philosopher 1");
}
A programban a filozófusokat egy-egy programszállal modellezzük, az 5 db pálcika foglaltságát pedig egy 5-ig számláló szemafor tartja nyilván.
A stdio hozzáfárás védelméhez szükségünk van egy mutexre is, mivel az ARM Cortex-M0 mikrovezérlők esetében használt C microlib programkönyvtár nem rendelkezik beépített hozzáférés védelemmel. A kiíratásokat ezért csak a notify() függvény hívásán keresztül végezzük.
A mytime absztrakt időzítő arra szolgál, hogy mellékesen a futó időt is kijelezzük. Ennek segítségével az eseményeket egy időskálán is elhelyezhetjük.
A programszálak törzsét az előző fejezet végén bemutatott mintaprogramhoz hasonlóan ugyanaz a függvény képezi. A "filozófusaink" tehát csak neveikben különböznek, illetve a véletlenszám generálásnak köszönhetően különböző időpontokban kezdeenek gondolkodni, vagy étkezni.
Étkezéskor két pálcikát kell megszerezni, ezért kétszer csökkentjük az s szemafor értékét az s.wait() függvényhívással. Ennek megfelelően étkezés végén is kétszer növeljük a szemafor értékét (mindkét pálcikát letesszük...) az s.release() függvényhívással. A szemafor kezdeti értékét a konstruktor meghívásakor állítjuk be 5-re.
A programban a gondolkodási idő 1 - 1,5 másodperc, az étkezés pedig 0,5 - 1 másorpec közötti véletlen időtartamú.
A program futási eredménye az alábbi ábrán látható. Minden kiírás arról értesít bennünket, hogy valamelyik filozófus megszerezte az evőeszközöket és elkezdett étkezni a jobboldali oszlopban megadott időpontban (a program indításától eltelt idő másodpercben értendő).
3. ábra: A 10_rtos_dpp program futási eredménye
Holtpont esetleges kialakulásakor a kiírás leáll (mi nem tapasztaltunk ilyet, de elvileg nem lehetetlen...). A kiéheztetés pedig abban nyilvánulna meg, hogy valamelyik programszál huzamosabb ideig nem fér hozzá az evőeszközökhöz. Ilyet sem tapasztaltunk, de a fenti program több vonatkozásban is tökéletesítésre szorulna. Mivel célunk itt csak a szemafor használatának bemutatása volt, az étkező filozófusok problémakörét nem igyekszünk teljeskörűen kimeríteni.
Mintapélda: Megosztott erőforrások kezelése
Ahogy a bevezető részben már említettük, egy szemafor önmagába nem elegendő két, vagy több egyforma megosztott erőforrás hozzáférésének védelmét ellátni, tehát a számláló szemafor nem tekinthető a mutex általánosításának. Az előző mintapéldában csak azért volt elegendő egy szemafor, mert csupán a szabad erőforrások számát tartottuk nyilván, azza nem törődtünk, hogy konkrátan melyik szabad.Az alábbi pédában hátom programszál verseng két erőforrás valamelyikének használatáért, s természetesen tudni akarjuk, hogy mikor, melyik programszál melyik erőforráshoz fért hozzá. A szabad erőforrások számát most is egy sem nevű szemafor segítségével tartjuk nyilván, a két erőforrás foglaltságát illetve a hozzáférés kizárólagosságát egy-egy mutex (m1 és m2) használatával adminisztráljuk. A foglalások eredményét kiíratjuk, ezért a szokásos módon az stdio hozzáférésének védelméhez is szükségünk lesz egy mutexre (stdio_mutex).
Ha egy programszálnak sikerült hozzáférési jogot szereznie, akkor még ki kell derítenie, hogy a két erőforrás közül melyik szabad. Ehhez a mutex objektum trylock() metódusát fogjuk használni, amelyik nem várakozik, hanem egy logikai értékkel jelzi, hogy sikeres volt-e a foglalás. Ha az első próbálkozásunk sikertelen volt, akkor a másik erőforrásnak kell szabadnak lennie, ezért ott már bátran használhatjuk a lock() metódust.
Mi itt a szemafor szerepe, ha az erőforrások tényleges lefoglalását úgyis egy-egy mutex segítségével végezzük? Elsősorban az, hogy a beérkező kérelmeket sorbarendezze. Ennek két haszna van:
- Nem fordulhat elő, hogy az egyik erőforrásra többen is várakoznak, miközben a másik erőforrás szabadon áll.
- A szemafor egyetlen várakozási sorából az ütemezőnek lehetősége
van arra, hogy mindig a legmagasabb prioritású programszálat engedje
tovább.
Hardver követelmények:
- FRDM-KL25z kártya
#include "mbed.h"
#include "rtos.h"
Semaphore sem(2); //manges two tokens
Mutex m1, m2; //two mutexes for two resources
Mutex stdio_mutex; //Control shared access to printf
DigitalOut led1(LED1); //Red LED
DigitalOut led2(LED2); //Green LED
DigitalOut led3(LED3); //Blue LED
DigitalOut ledarray[]= {led1,led2,led3}; //Just needed for indexing the objects...
Timer mytime;
void notify(int tid, int res) {
stdio_mutex.lock();
if(res > 0) {
printf("Task %d: acquired << resource %d. at %8.1f\n\r",tid,res,mytime.read());
} else {
printf("Task %d: released >> resource %d. at %8.1f\n\r",tid,-res,mytime.read());
}
stdio_mutex.unlock();
}
void mythread(void const* args) {
while (true) {
Thread::wait(500+rand()%500);
ledarray[(int)args-1]=0; //LEDx on
sem.wait(); //Wait for token
if(m1.trylock()) { //Try to lock mutex #1
ledarray[(int)args-1]=1; //LEDx off
notify((int)args,1);
Thread::wait(1000+rand()%500);
notify((int)args,-1);
m1.unlock();
} else {
m2.lock(); //Wait for mutex #2
ledarray[(int)args-1]=1; //LEDx off
notify((int)args,2);
Thread::wait(1000+rand()%500);
notify((int)args,-2);
m2.unlock();
}
sem.release(); //Release token
}
}
int main (void) {
led1 = 1;
led2 = 1;
led3 = 1; //Switch off all LEDs
mytime.start(); //Start timer
Thread t2(mythread,(void *)2U); //Define and run thread2
Thread t3(mythread,(void *)3U); //Define and run thread3
mythread((void const*)1U); //Run thread1
}
Megjegyzések: - A led1, led2, led3 objektumok a FRDM-KL25Z kártyánál az RGB LED színkomponenseit jelentik. Indexelhető kezelésükhöz egy tömbbe rendeztük ezeket az egyébként független objektumokat. Lehetőségként felmerülhet a Digitális I/O fejezetben ismertetett BusOut objektumosztály használata is, azonban nem világos, hogy ennek megvalósítása hogyan viselkedik többfeladatos környezetben.
- Az RGB LED mindig az éppen várakozó programszál színét (1: vörös, 2: zöld, 3: kék) jelzi ki.
4. ábra: Az 10_rtos_semaphore program futási eredménye
Mintapélda: Futási sorrend biztosítása
A szemaforok tipikus felhasználásai a programszálak szinkronizálására szolgálnak. Ilyen lehet például:- A programfutás sorrendiségének biztosítása - ahol a programszálak úgy adogatják egymásnak a szemafor zsetonokat, mint a staféták a váltóbotot a váltófutásnál.
- Randevú (rendezvous) - ahol a szemaforok azt biztosítják, hogy két programszál egy adott pontban várja be egymást.
- Sorompó (barrier) - ahol az a cél, hogy több párhuzamosan futó programszál egy adott pontban várja be egymást.
Hardver követelmények:
- FRDM-KL25z kártya
#include "mbed.h"
#include "rtos.h"
Semaphore s1(1); //allow task1 to run
Semaphore s2(0); //s2 has no token at start
Semaphore s3(0); //s3 has no token at start
DigitalOut led1(LED1); //Red LED
DigitalOut led2(LED2); //Green LED
DigitalOut led3(LED3); //Blue LED
void thread1(void const* args) {
while (true) {
s1.wait();
led1 = 0; //Red LED ON
Thread::wait(500+rand()%500);
led1 = 1; //Red LED OFF
s2.release();
}
}
void thread2(void const* args) {
while (true) {
s2.wait();
led2 = 0; //Green LED ON
Thread::wait(500+rand()%500);
led2 = 1; //Green LED OFF
s3.release();
}
}
void thread3(void const* args) {
while (true) {
s3.wait();
led3 = 0; //Blue LED ON
Thread::wait(500+rand()%500);
led3 = 1; //Blue LED OFF
s1.release();
}
}
int main (void) {
led1 = 1; led2 = 1; led3 = 1; //Switch off all LEDs
Thread t2(thread2); //Define and run thread2
Thread t3(thread3); //Define and run thread3
thread1(NULL); //Run thread1
}