Az Aikido programozási nyelv

Párhuzamosság

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.

Szálak

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:

thread test() { System.println (“thread “ + getID() + “ sleeping”) sleep (10 * 1000000) // 10 seconds System.println (“woke up”) } var t = test() System.println (“waiting for test”) join (t) System.println (“test completed”)

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:

waiting for test
thread 4 sleeping
[10 másodperc eltelik]
woke up
test completed

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.

Szál prioritások

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)

Alternatív modell

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:

class Thread { public function run() { // a leszármaztattot osztálynak kell felülírnia throw “run called directly in thread” } private var id = 0 // helyi másolata a theadid-nak public thread start() { // a thread indítása id = getID() run() } public function setPriority (p) { main.setPriority (p) } public function getPriority() { return main.getPriority() } }

Ezt a definíciót felhasználva, származtathatunk egy osztály a Thread osztályból, hogy megcsináljuk saját thread-ünket:

class ServerThread (stream) extends Thread { public function run() { for (;;) { // a szerver szál funkcionalitása } } } // 2 példány a ServerThread-ből var t1 = new ServerThread (stream1) var t2 = new ServerThread (stream2) // a példányok indítása t1.start() t2.start()

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ó.

Monitorok

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:

monitor Count { var count = 1 // a védett count változó public function inc() { // függvény a count növelésére count++ } } // A monitor használata var count = new Count() // egy példány a monitorból thread A { // A thread for (;;) { count.inc() // count növelése } } thread B { // B thread for (;;) { count.inc() // count növelése } }

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:

thread A { for (;;) { count.count++ } }

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:

count.count = count.count + 1

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.

Várakoztatások

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):

monitor Tracks { var tracks = [] // a számok vektora var numTracks = 0 // a számok vektor mérete public function addTrack(t) { t -> tracks // a t szám hozzáadása a tracks vektorhoz numTracks++ // növekedett a vektor mérete notify() // figyelmeztetés, most már van szám } public function getTrack() { while (numTracks == 0) { // várakozás, amíg nincs új szám wait() } var t = tracks[0] // megfogjuk az első számot delete tracks[0] // ki is vesszük a listából numTracks— // csökkent a vektor mérete return t // visszaadjuk a számot } } var tracks = new Tracks() // a lejátszási lista egy példánya // A lejátszó – minden szám lejátszása végtelen ciklusban thread player() { for (;;) { var track = tracks.getTrack() System.println (“playing track “ + track) } } // A kliens – hozzáad 5 számot a listához thread client (name) { foreach x 5 { // 5 szám lesz tracks.addTrack (name + x) sleep (System.rand() % 10000000 + 500000) // várakozás random ideig } } // A lejátszó thread elindítása var p = player() // 4 db kliens thread elindítása foreach c 4 { client (“client” + c) } // Várakozás a többi szálra, hogy befejeződjenek (soha nem fognak) join (p)

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:

playing track client00
playing track client01
playing track client30
playing track client20
playing track client10

Szemaforok

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ó:

monitor Semaphore (count = 0) { public function take() { // lefoglalunk egy helyet while (count <= 0) { wait() } count—- } public function put() { // elengedünk egy helyet count++ notify() } } var res = new Semaphore (10) function grabResource() { res.take() } function releaseResource() { res.put() }

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.

Mutexek

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ó:

monitor Mutex { var available = true // flag, hogy mikor elérhető // lock és return amikor elérhető public function lock() { while (!available) { wait() } available = false } // unlock. Csak akkor jöhet ide, ha már lock-olt public function unlock() { available = true notify() } } var countLock = new Mutex() function incCount() { countLock.lock() count++ countLock.unlock() }

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.

Szinkronizáció

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:

synchronized class Semaphore { //... }

Ha nem az egész osztály, hanem csak egy tagfüggvényt, akarunk monitorként kezelni, akkor az is megvalósítható:

class Concordance { public: synchronized function add (word) { // a példány kizárólagosan lock-olt erre az időre } }

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:

synchronized (obj) { // az obj objektum itt kizárólag a mienk }

Szál folyamok

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:

thread something() { //… } var t = something()

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:

// szerver thread, rácsücsül az input-ra, feldolgozza és kiírja az eredményt thread server { while (!System.eof(input)) { // feldolgozás, amíg nincs vége a stream-nek var command = “” input -> command // parancs kiolvasása a stream-ről var result = execute (command) // parancs végrehajtása result -> output // eredmény kiírása az output-ba System.flush (output) // stream ürítése } } var serverStream = server() // szerver thread indítása var result = “” “cat x.c\n” -> serverStream // parancs küldése a szervernek System.flush (serverStream) // stream ürítése serverStream -> result // várakozás az eredményre

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.