Az Aikido a többszálú programozást nyelvi szinten támogatja. Hasonlóan, mint néhány őse - az Ada és a Java - beépített konstrukciókkal rendelkezik, melyekkel valódi többszálú programok készíthetőek.
Az alap konstrukció egy többszálú programban, a thread (szál). A thread tulajdonképpen egy függvény, párhuzamosan hívódik meg és fut le, a többi másik thread-del a programban. Egy program akármennyi thread-et használhat. Egy Aikido programban minden egy thread-ben fut. A főprogram maga is egy thread. Az Aikido több műveletet biztosít a szálak vezérlésére. Ezek a main package-ben találhatók:
Név | Paraméterek | Leírás |
sleep | idő milliszekundumban - integer | Szál futásának felfüggesztése egy időre. |
setPriority | prioritás - integer | Az aktuális szál prioritásának beállítása. |
getPriority | nincs | Az aktuális szál prioritását adja vissza. |
getID | nincs | Az aktuális szál azonosítóját adja vissza (ez egy szám) |
join | szál folyam - stream (lásd Szál folyamok) | Várakozás arra, hogy a megadott thread termináljon |
Vegyük a következő példát:
A példában a „test” thread kiírja az azonosítóját, és vár 10 másodpercet. Utána „felébred” és terminál. A főprogram indít egy „test” thread példányt és megvárja, amíg befejeződik. Fontos megjegyezni, hogy amennyiben a join hiányozna, a program azonnal befejeződne anélkül, hogy megvárná a „test” thread példányának befejeződését (a főprogram befejeződésénél minden másik thread futása is befejeződik).
A példaprogram egyik lehetséges futási eredménye:
Az első két sor sorrendje nem determinisztikus, hiszen a szál elkezdődhet azelőtt is, hogy a főprogram kiírja az üzenetet.
Minden thread ténylegesen párhuzamosan hajtódik végre. Minden thread-hez tartozik egy ütemező prioritás. Ez azt jellemzi, hogy a thread mennyi CPU időt kap a többi szálhoz viszonyítva. Az Aikido 100 prioritási szintet különböztet meg, 0 a legalacsonyabb, 99 a legmagasabb prioritás. Egy magasabb prioritással rendelkező thread több CPU időt kap, mint egy alacsonyabb prioritással rendelkező.
A thread, a prioritást csak saját magának tudja beállítani (egy thread nem nyúlhat másik thread-nek a prioritásához). A prioritás maga egy integer a 0..99 intervallumban. Ahogy azt az előző táblázat leírta, a setPriority() függvény állítja be a prioritást, és a getPriority() függvénnyel lehet lekérdezni.
Segítségként, a következő konstansok definiáltak, a System package-ben:
Név | Érték (jelentés) |
MIN_PRIORITY | 0 (a legalacsonyabb prioritás) |
MAX_PRIORTY | 99 (a legmagasabb prioritás) |
Az Aikido thread modelljét, könnyen a Java-ban használt Thread Object modellhez hasonló formára hozhatjuk. Vegyük a következő definíciót:
Ezt a definíciót felhasználva, származtathatunk egy osztály a Thread osztályból, hogy megcsináljuk saját thread-ünket:
Ez a Thread osztály természetesen, csak egy egyszerűsített változata a Java-ban használt megfelelőjének. De ez egy jó példa arra, hogy lássuk az Aikido thread modellje elég sokoldalú és jól használható.
A szálak gyakran osztanak meg adatokat egymással. Ilyen esetben nagy az esélye annak, hogy 2 vagy több szál ugyanabban az időpillanatban ugyanazt az adatot akarja elérni (módosítani, olvasni). Az Aikido monitor-ja lehetővé teszi, hogy a megosztott adatokat egyszerre csak egy szál használja (kölcsönös kizárás).
A monitor nemcsak atomi zárat (mutex) biztosít az adatok védelmére, hanem várakozási funkciókat is, amelyek segítségével a megosztott erőforrás allokációs problémák is megoldhatóak (lásd következő alfejezet).
A monitor úgy is tekinthető, mint a memóriának egy szelete, amihez egyszerre csak egy szál férhet hozzá. Minden hozzáférési kísérlet, ami akkor történik, amikor már valamelyik másik szál éppen használja a memória részt, egy várakozási sorba kerül, amíg nem kap jogot a hozzáférésre. A hozzáféréseket kezdeményező szálak ilyenkor blokkolódnak, azaz várakoznak.
Egy egyszerű példa: legyen egy count változónk, amit egyszerre több szál akar növelni, és biztosítsuk a kölcsönös kizárást erre a count változóra! Monitor segítségével, ez a következőképpen nézhet ki:
A példában 2 thread szerepel, melyek végtelen ciklusban növelik a count értékét. Figyeljük meg, hogy a változó növeléséhez egy függvényt használunk. Ha nem így tennénk, hanem közvetlenül a count változóra hivatkoznánk a monitor-on belül, akkor így nézne ki egy szál:
De ez nem jó! Ezzel nem oldódik meg a probléma, mert a ++ operátor nem atomi, hanem a következő utasításra bomlik szét:
Az igaz, hogy a count változó hozzáféréséhez a monitor kölcsönös kizárást biztosít – tehát csak egy szál használhatja egyszerre – de így a hozzáférés 2 lépést jelent: olvasás, írás. És hiába a kizárás, az olvasás és az írás között még bármi történhet a változóval. Ezért került a növelés külön függvénybe.
A megosztott erőforrás allokációk támogatására, a következő függvények használhatóak egy monitor-on belül:
Ezen függvények mindegyike monitor-on belül használható, tehát csak akkor hívódnak meg, ha a thread megkapta a monitor kizárólagos hozzáférési jogát.
A következő példán keresztül jobban megérthető e függvények működése: vegyük egy zenelejátszót. Ehhez a lejátszóhoz tartozik egy lejátszási lista, amit egy szál folyamatosan figyel, és lejátsza az aktuális zeneszámot a listából, majd utána ki is veszi a listából. Vannak kliens szálak, amik a lejátszási listára helyeznek el újabb és újabb zeneszámokat. Ezt szimuláljuk Aikido-ban!
A teljes program, ami ezt megvalósítja következő (legyen, mondjuk 1db kliens, aki 5db zeneszámot helyez el a lejátszási listán és a zeneszámokat reprezentálják számok):
A programkódot elemezve látható, hogy ha egy thread a monitort a wait() utasítással engedte el, akkor a következő notify() hívásnál, amit megkap, a wait() után folytatódik tovább a futása.
A program kimenete (nem determinisztikusságnak köszönhetően) a következő lehet:
A szemafor, többszálú programok készítése esetén gyakran használt eszköz. Az Aikido monitor segítségével, ez könnyen implentálható:
Fontos lehet megjegyezni, hogy a put() függvényben a notify() használata garantálja, hogy az erőforrás foglalások FIFO sorrendben történnek, azaz aki a legelsőként jelezte foglalási igényét, az kapja meg a leghamarabb az erőforrást. Ha notifyAll() használata esetén, bármelyik várakozó szál megkaphatná az erőforrást.
Egy mutex, tulajdonképpen egy olyan szemafor, ami 1 thread hozzáférését engedi egy időben az adott erőforráshoz. A szemafor monitor-ral történő megvalósítása alapján, egy egyszerű mutex megvalósítása könnyen megadható:
A példában az incCount() függvény kizárólagos hozzáférés mellett növeli meg a count változó értékét.
Fontos lehet megjegyezni, hogy az unlock() függvényben a notify() használata garantálja, hogy az erőforrás foglalások FIFO sorrendben történnek, azaz aki a legelsőként jelezte foglalási igényét, az kapja meg a leghamarabb az erőforrást. Ha notifyAll() használata esetén, bármelyik várakozó szál megkaphatná az erőforrást.
A monitor-ok használatán kívül, a synchronized kulcsszóval is biztosítható a kölcsönös kizárás.
Például, a következő osztály, egy monitorként viselkedik:
Ha nem az egész osztály, hanem csak egy tagfüggvényt, akarunk monitorként kezelni, akkor az is megvalósítható:
A synchronized kulcsszóval tulajdonképpen ideiglenes zárakat (lock) hozhatunk létre. Nemcsak osztályokra és függvényekre, hanem más objektumokra is:
Az Aikido-ban a megoszott objektumok használatával valósítható meg a kommunikáció több szál között. De nem ez az egyetlen módja. A stream-ek (folyam) segítségével is lehet több szál között kapcsolatot teremteni.
Minden thread-hez tartozik 2db stream: input, output. Amikor egy thread-et elindítunk, akkor visszatérési értékként megkapjuk a thread input stream-ét:
Ilyenkor a t változóba a thread-hez tartozó input stream kerül. Emlékezzünk vissza az eddigi példákra, melyekben join() szerepelt! Így már talán érthető, hogy a join() paraméternek miért egy stream-et vár.
A fő szálhoz (main) tartozó input stream az stdin-re, az output stream az stdout-ra van irányitva.
Példa a thread stream-ek kezelésére:
A példában definiált server thread az input stream-jére parancsokat vár, és azokat végrehajtja. A program a „cat” parancsot elküldi a server thread-nek és megvárja, amíg eredményt ad vissza, majd a visszakapott eredményt eltárolja.