RTOS Mutex

A fejezet tartalma:
Az előző fejezet végén bemutatott példaprogram három programszálat futtatott, ám ezek egymástól függetlenül végezték feladatukat, ugyanúgy, mintha három külön mikrovezérlő kártyán futottak volna. Az RTOS alkalmazások többsége azonban nem ilyen, a benne futó programszálak egy nagy feladat részfeladatait végzik, így alkalmanként szükség lehet rá, hogy tevékenységeiket egymáshoz szinkronizálják, a közös használatú erőforrásokon megosztozzanak, vagy adatot cseréljenek egymással. Az mbed-RTOS fejlett eszköztárat biztosít ezekhez a feladatokhoz (jelzőbitek, mutex, szemafor, levelesláda és üzenetek formájában), melyek közül most a megosztott erőforrások kezelésére használt mutex használatát tárgyaljuk.

Mutex (kölcsönös kizárás)

A mutex név a kölcsönös kizárás angol elnevezésének (mutual exclusion) rövidítése. Többfeladatos programozásnál előfordul, hogy két vagy több programszál egyidejűleg ugyanazt az erőforrást akarja használni, ami nem engedhető meg, mert pl. összekeverednének a soros porton vagy az USB porton kiküldött adatok. Ilyenkor versengés alakul ki, s az erőforráshoz hamarabb forduló programszál egy mutex változó segítségével lefoglalja az eszközt, majd használat után megszünteti a lefoglalást. A kritikus szakaszhoz később érkező programszál pedig várakozásra kényszerül, amíg az erőforrás fel nem szabadul. A programszálak tehát kölcsönösen kizárják egymást a kritikus szakaszon, nem fordulhat elő, hogy egyidejűleg vezérlik a közös használatú perifériát.

1. ábra: Erőforrás megosztása mutex használatával

A mutex tulajdonképpen az általánosabb szemaforok speciális esete, amikor a szemafor csak két állapotot vehet fel: 0 és 1. Ezért bináris szemafornak is nevezik (szemben az ún. számláló szemaforral, amely többféle értéket is felvehet).

A Mutex objektumosztály tagfüggvényei

A kölcsönös kizárást biztosító mutexek létrehozása és kezelése a Mutex objektumosztály segítségével végezhető, tagfüggvényeit az alábbi táblázatban foglaltuk össze.
Függvénynév
Funkció
Mutex név
Létrehoz és inicializál egy "név" nevű mutex  objektumot
lock(time)
Várakozik, amíg a mutex elérhetővé válik. Ha a mutex foglalt, 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.
trylock()
Megpróbálja lefoglalni a mutex példányt, de nem várakozik, ha nem sikerül.
unlock()
Felszabadítja a korábban lefoglalt mutex példányt.

Megjegyzések és korlátozások:

Mintapélda a mutex használatára

Az alábbi mintaprogram bemutatja a mutex használatát az stdio hozzáférés védelmére (printf() függvény használata). A program lefordításához importálni kell az mbed-RTOS programkönyvtárat.

Hardver követelmények:
1. lista: A 10_rtos_mutex/main.cpp program listája
#include "mbed.h"
#include "rtos.h"

Mutex stdio_mutex;

void notify(const char* name, int state) {
stdio_mutex.lock();
printf("%s: %d\n\r", name, state);
stdio_mutex.unlock();
}

void test_thread(void const *args) {
while (true) {
notify((const char*)args, 0); Thread::wait(1000);
notify((const char*)args, 1); Thread::wait(1000);
}
}

int main() {
Thread t2(test_thread, (void *)"Th 2");
Thread t3(test_thread, (void *)"Th 3");
test_thread((void *)"Th 1");
}
A program több újdonságot tartalmaz, amelyek magyarázatra szorulnak:

2. ábra: A 10_rtos_mutex program futási eredménye

Mintapélda: Az étkező filozófusok problémája

Az étkező filozófusok esete (az angol szakirodalomban: Dining Philosopher Problem néven ismert) egy olyan probléma, ami könnyen és gyorsan megérthető, de felmerülő problémái valós informatikai problémákká növik ki magukat. A történet így szól: Egy tibeti kolostorban öt filozófus él. Minden idejüket egy asztal körül töltik. Mindegyikük előtt egy tányér, amelyből sohasem fogy ki a rizs. A tányér mellett jobb és bal oldalon is egy-egy pálcika található a 3. ábra szerinti elrendezésben. A filozófusok életüket az asztal melletti gondolkodással töltik. Amikor megéheznek, étkeznek, majd ismét gondolkodóba esnek a következő megéhezésig. És ez így megy az idők végezetéig. Az étkezéshez egy filozófusnak meg kell szereznie a tányérja melletti mindkét pálcikát. Ennek következtében amíg eszik, szomszédai nem ehetnek. Amikor befejezte az étkezést, leteszi a pálcikákat, amelyeket így szomszédai használhatnak.

3. ábra: Étkező filozófusok

Elemezzük, milyen problémákkal szembesülhetnek a filozófusaink:
Az alábbi programban a filozófusokat egy-egy programszál képviseli, melyek többé-kevésbé véletlenszerű időtartamig gondolkodnak, majd amikor megéheznek, megpróbálják megszerezni a tányéruk két oldalán található pálcikákat (a pálcikákat egy-egy mutex reprezentálja, s a mutex sikeres lezárása az általa képviselt pálcika megszerzését jelenti). Ha sikerült megszerezni mindkét pálcikát, akkor étkezés következik (ennek időtartama is véletlenszerű), majd letesszük a pálcikákat, s újra a gondolkodásnál folytatjuk.

Könnyen belátható, hogy a holtpont elkerülése a programozó felelőssége. Ha például egyszerre minden filozófus azzal kezdi, hogy megragadja a jobb kezénél levő pálcikát, akkor máris holtpontra jutottunk! Olyan stratégiát kell tehát kidolgozni, amelyben szerepet kap a kooperativitás - azaz ha valamelyik filozófus nem tudja megszerezni mindkét pálcikát, akkor leteszi a kezéből az elsőnek felvettet is, hogy a szomszédai étkezni tudjanak. Ez önmagában még kevés a holtpont elkerüléséhez, mert ha mindegyik filozófus felveszi a jobb kezénél levő pálcikát, majd - intelligens emberek lévén - belátják, hogy ez holtpont, ezért leteszik a pálcikát, azután kezdik elölről, akkor nem haladtunk sokra! Közelebb jutunk a megoldáshoz, ha azt a módszert alkalmazzuk, amit a hálózati kártyák ütközés detektálásakor használnak: véletlenszerűen választott ideig várakoznak, majd újra próbálkoznak.

A i. programszál futásakor mutex lefoglalásánál most nem a végtelen várakozást választjuk, hanem a trylock() metódussal ellenőrizzük, hogy sikerült-e a jobboldali pálcikához tartozó i-1. mutexet lefoglalni. Ha sikerült, akkor megpróbáljuk lefoglalni a másikat (az i. mutexet) is. Ha ez is sikerült, akkor kezdődhet a falatozás, majd letesszük a pálcikákat és kezdődhet a gondolkodás. Ha viszont a második mutexet nem sikerült lezárni, akkor a kooperativitás jegyében feloldjuk az első mutexet is (nehogy holtpont alakuljon ki), majd véletlen hosszúságú ideig várunk, mielőtt újra próbálkozunk.


Hardver követelmények:
2. lista: A 10_rtos_dpp_mutex/main.cpp program listája
#include "mbed.h"
#include "rtos.h"
Mutex stdio_mutex;
Mutex chopstick[5]; //Array of mutexes representing the 5 chopsticks

void notify(int num, int state) {
stdio_mutex.lock();
if(state) {
printf("Philospher %d is EATING \n\r", num);
}
else {
printf("Philospher %d is thinking \n\r", num);
}
stdio_mutex.unlock();
}

void philosopher(void const *args) {
while (true) {
if(chopstick[(int)args-1].trylock()) {
if(chopstick[(int)args%5].trylock()) {
notify((int)args,1); //Start EATING
Thread::wait(1000+rand()%1000);
chopstick[(int)args%5].unlock(); //Release chopsticks
chopstick[(int)args-1].unlock();
notify((int)args,0); //Start Thinking
Thread::wait(2000+rand()%2000); //Get's hungry after this time...
}
else {
chopstick[(int)args-1].unlock();
Thread::wait(100+rand()%100); //Wait for random time if failed
}
}
else {
Thread::wait(100+rand()%100); //Wait for random time if failed
}

}
}

int main()
{
Thread t2(philosopher, (void *)2U);
Thread t3(philosopher, (void *)3U);
Thread t4(philosopher, (void *)4U);
Thread t5(philosopher, (void *)5U);
philosopher((void *)1U);
}

Megjegyzések:
A program egy futásának eredménye az alábbi ábrán látható:

4. ábra: A 10_rtos_dpp_mutex program futási eredménye