A Habanero Java programozási nyelv

Párhuzamosság

A Habanero Java csak az explicit párhuzamosságot támogatja, viszont kód- és adatpárhuzamosságot is megvalósíthatunk a segítségével. Utóbbit elsősorban a vezérlési szerkezeteknél ismertetett ciklusszervező utasításokkal, melyek az itt bemutatandó kódpárhuzamosítási lehetőségekre épülnek.

A phaserek és accumulatorok használatát bemutató példasorozat található a példakódok között.

Párhuzamos tevékenységek

async [at(place)] [phased[(ph_1<mode_1>,...)]] [when(condition)] Stmt;

Párhuzamos tevékenységeket a fenti szintaxisnak megfelelő utasítással hozhatunk létre. A Stmt által reprezentált utasítás egy külön tevékenységben kerül végrehajtásra, a szülő pedig azonnal tovább mehet.
A tevékenységek nem Java Threadeket jelentenek, hanem activity típusú objektumokat, melyeket a futtató környezet a választott végrehajtási hely dolgozó száljaira ütemez.

Egy tevékenység a létrehozásakor opcionálisan több dolgot is meghatározhatunk:
Az at kulcsszó után megadhatjuk, hogy a tevékenységet melyik helyen futtassa a futtató környezet. (Az at kulcsszó csak az 1.2-es verzió szintaxisában jelent meg.) Hely megadása nélkül az aktuális helyre kerül az új tevékenység.
A phased kulcsszó segítségével az új tevékenységet phaserekhez kapcsolhatjuk. Az új tevékenység phaserekhez való kapcsolásának szabályairól a phaserek műveletei alfejezet leszármazott kapcsolása című részében olvashatunk.
A when kulcsszó után megadott feltétellel a tevékenység indítása késleltethető. A futtató környezet addig vár az új tevékenység létrehozásával, amíg az elvileg mellékhatás-mentes feltétel igazzá nem válik. Persze a mellékhatás-mentesség nem ellenőrzött, így a következő dolog simán megtehető:

num.i = 0; //num egy referencia, aminek i nevű adattagja integer típusú async when(++num.i == 500) { System.out.println("Child " + num.i); } System.out.println("Main " + num.i);

Ennek egy lehetséges kimenete:

Main 331 Child 500

Kommunikáció

A párhuzamos tevékenységek kommunikációjában semmi újdonságot nem hoz a Habanero Java. Alapvetően közösen használt változókon (objektumokon) keresztül történhet a kommunikáció, de lehetőség van a tevékenységek végeredményének szinkronizálására is.

Közös változók

Az 1.2-es verzió előtt minden olyan változót, amit a hatókörében lévő párhuzamos szerkezetben használni akartunk, final módosítóval kellett deklarálni. Az 1.2.0-ás verzióban ezt a megszorítást kezdik feloldani. A fordító néha, főleg bonyolultabb szerkezetek esetén valamiért azonban még mindig kiköveteli a final használatát, ezért célszerűbb mindig kiírni.
Mindenesetre a tevékenységeknek saját példánya van a külsőbb szinten deklarált változókból, amiket nem változtathatnak meg. Így primitív típusú változókat nem lehet kommunikációra használni, mivel azok nem referenciák, tehát mindenki csak a saját példányát tudja elérni, amit ráadásul nem is változtathat.

Persze nem feltétlenül vezet sok jóra, ha mindenki tetszése szerint hozzáférhet a közösen használt objektumokhoz. Bizonyos esetekben célszerű lenne megvalósítani az objektumokhoz való hozzáférés kölcsönös kizárását. A Habanero Java ehhez jelenleg nem sok eszközt kínál. A változókat nem lehet zárolni, és a Javából ismert synchronized sem használható, mivel a párhuzamosságot legfelső szinten nem Threadek, hanem activityk valósítják meg. A kölcsönös kizárás kódrészletek szintjén valósítható meg az isolated konstrukcióval, de ez nem túl hatékony megoldás.

Végeredmények

final future<T> f = async<T> [at(place)] [phased[(ph_1<mode_1>,...)]] [when(condition)] Expr;

Aszikron számítás eredményének kinyerésére szolgáló konstrukció. A future típusú változókat mindig final módosítóval kell deklarálni. Az at, phased, when módosítók szerepe megegyezik az egyszerű async konstrukciónál megismertekkel. Lényegében a sima async egy async<void>, csak nem kell kiírni. (Emellett a sablonszerű szintaxis mellett érdemes egy pillantást vetni a sablonok Habanero Java általi támogatottságára is, ami jelenleg nincs.)
A párhuzamosan indított kifejezésnek returnnel kell végződnie, ami természetesen egy T típusú kifejést kell tartalmazzon.

Az eredmény kinyerésére az future objektum get() metódusa szolgál, ami megvárja a kifejezés kiértékelését, majd megadja a párhuzamos számítás által kiszámított értéket. Fontos, hogy a get() blokkoló utasítás, a számítás befejezettségének ellenőrzésére a nem-blokkoló finished() metódus szolgál.

A get() blokkoló jellege miatt a későbbiekben ismertetett kölcsönös kizárást felhasználva könnyen holtpontot idézhetük elő:

final future<int> f = async<int> { isolated { System.out.println("async"); } return 1; }; isolated { System.out.println(f.get()); }

Látható, hogy ha az async konstrukción belüli isolated utasítás ütemeződik előbb, akkor a kódrészlet gond nélkül lefut. Azonban ha a másik kölcsönösen kizáró blokkba kerül először a vezérlés, akkor a future sosem juthat el a kiértékelése végére a benne szereplő isolated miatt, így holtpont keletkezik.

Szinkronizáció

A párhuzamos tevékenységek szinkronizálására a Habanero Java több lehetőséget is kínál. Ezek között szerepel a kölcsönös kizárás, a tevékenységek közti join megvalósítása a finish konstrukcióval, valamint a pont-pont és barrier szinkronizációt egységesítő phaser konstrukció használata.

Kölcsönös kizárás

isolated Stmt;

Az isolated kölcsönös kizárást valósít meg a vele megjelölt utasítások végrehajtása között.

A konstrukció az izolált utasítások gyenge atomicitását valósítja meg, azaz izolált utasításból elérhető nem-izolált utasítás is.
Az izolált utasításoknak teljesíteniük kell azt a megkötést, hogy nem tartalmazhatnak párhuzamos szerkezetet.

Finish

finish Stmt;

A join konstrukció megvalósítása a tevékenység és leszármazottai között. A vele megjelölt utasítás végrehajtása után csak akkor halad tovább a vezérlés, ha az összes, az utasításból (akár közvetve) származó tevékenység véget ért.

Phaserek

A phaserek bevezetésével a Habanero Java egy egységesített szinkronizációs eszközt ad a programozóknak, mely a következő tulajdonságokkal rendelkezik:

A phaserekkel kapcsolatos fogalmak első látásra talán bonyolultnak tűnnek, ezért a használatuk megvilágítására található egy példa a példakódok között.

Phaserek szerkezete

Sok tevékenység szinkronizálásánál szűk keresztmetszet lehet az egyetlen phaser, ennek elkerülésére hierarchikusan egymás alá szervezett phasereket lehet használni a terhelés elosztására. Ilyen struktúrát a phaser osztály maga is képes kezelni.
A phaserek szerkezetének meghatározására használható osztályoknak a phaserConfig interfészt kell megvalósítaniuk. A fent vázolt hierarchikus faszerkezetbe szervezett phaserek megvalósítását a phaserTreeConfig osztály biztosítja.

hj.lang.phaserConfig cfg = new hj.lang.phaserTreeConfig(numTiers, numDegree);

A fa szintjeinek számát és a csúcsok fokszámát meg kell adnunk a konstruktornak. Ezután az ilyen phaserConfig példánnyal létrehozott phaserek a megfelelő hierarchikus szerkezetet veszik fel.

Kapcsolódási módok

A phaser által nyújtott szolgáltatások eléréséhez nem elég ismernünk az phaser referenciáját, az adott tevékenységet kapcsolnunk kell a phaserhez. Egy tevékenység a következő módok valamelyikével csatlakozhat egy phaserhez:

Ahogy látszik a signal-only és a wait-only módok egymás kiegészítései, a signal-wait pedig a kettő egyesítése. A signal-wait-next mód pedig még egy plusz szolgáltatást ad a jelzés és várakozás képessége mellé. Ez egy erősorrendet határoz meg az egyes módok között, aminek a kapcsolódás leszármazottakra történő delegálása esetén lesz jelentősége.

Phaserek műveletei

A phaserekre a következő műveletek értelmezettek. Ezek legtöbbjét nem a konkrét phaser objektumon kell végrehajtani, a kapcsolt phasereknek a futtató környezet közvetíti az üzenetet.

Létrehozás
phaser ph = new phaser([phaserMode[, phaserConfig]]);

A konstruktor létrehoz egy új phaser objektumot, mely az inicializáció során az első fázisába kerül.
A phaserhez a megadott módon kapcsolódik a létrehozó tevékenység. A mód explicit megadása nélkül a legerősebb signal-wait-next móddal kapcsolódik a létrehozó tevékenység. Megadható egy phaserConfig objektum, mely a phaser belső struktúráját határozza meg. Enélkül egyetlen, centralizált phaser jön létre.

Leszármazott kapcsolása
async phased[(ph_1<mode_1>,...)] Stmt;

A phased async utasítás a létrehozandó tevékenységet a megadott phaserekhez a megadott módokon kapcsolja.
Csak olyan kapcsolódási mód adható tovább, amellyel a szülő is rendelkezik. (Capability Rule) A fentebb jelzett erősorrend értelmében minden erősebb mód magában foglalja a gyengébbeket. Erre a phaser-műveletek közti versenyhelyzetek elkerülése miatt van szükség.

A holtpontok elkerüléséhez szükséges tulajdonság megfogalmazásához bevezették az Immediately Enclosing Finish (IEF) fogalmát, ami minden végrehajtott utasítás dinamikus jellemzője. Azt a legközelebbi finisht jelöli, amelyben a konkrét utasítás végrehajtásra kerül. A holtpontok elkerüléséhez szükséges tulajdonság pedig az IEF Scope Rule, amely azt mondja ki, hogy csak olyan phaserhez kapcsolható egy újonnan létrehozott tevékenységet, mely a létrehozással azonos IEF-sel rendelkezik.

Amennyiben a fenti két szabályt megsérti egy tevékenység létrehozása, akkor PhaserException kivétel váltódik ki futási időben.

Megjegyzendő, hogy a phaserek és módok megadása nélkül a phased kulcsszó hatására a létrehozott tevékenység a létrehozóval teljesen megegyező módon fog kapcsolódni a phaserekhez.

Regisztráció törlése

Ha egy tevékenységet kapcsoltunk egy phaserhez, akkor azt nem tudjuk leválasztani róla. Egy tevékenység a terminálásakor végrehajt egy implicit next utasítást, és ekkor automatikusan törli magát az összes phaserről. Valamint ha egy tevékenység elhagy egy F finish-utasítást, akkor az összes, általa az F-ben létrehozott phaser törlésre kerül.

Next
next;

A tevékenység minden kapcsolt phasernek jelzi, hogy a következő fázisba szeretne lépni. A konkrét működés a kapcsolódás módjától függ.

signal-only
Megfelel egy signal utasításnak. Jelzi, hogy végzett az aktuális fázisával, és tovább is lép a következő fázisára.
wait-only
Várakozik, amíg a signal képességgel rendelkező tevékenységek mindegyike jelzi, hogy befejezte az aktuális fázist, ezután átlép a következő fázisába.
signal-wait[-next]
Az előző kettő kombinációja. Először elvégzi a signal műveletet, majd vár, amíg mindenki nem jelzi a továbblépési szándékát. Ha az előző phaser-művelete signal volt, akkor csak várakozni kezd.
Next with single statement
next single Stmt;

Csak signal-wait-next módban regisztrált tevékenység adhat meg single statementet. Hatása hasonló a next utasításhoz, azzal a kiegészítéssel, hogy a fázisátmenet során a megadott utasítás egyszer végrehajtódik. Teljesen természetes szabály, hogy ha ugyanazon phaserhez több tevékenység is megad single statementet, akkor azoknak azonosaknak kell lenniük.

Signal
ph.signal(); //az adott phasernek jelez signal; //az összes phasernek jelez

Ez az egyetlen phaser-művelet, amit lehet külön egy konkrét phaserre is alkalmazni az általános, összes phaserre történő alkalmazás mellett. A phaserekre gyakorolt hatása a kapcsolódás módjától függ.

wait-only
Nincs hatása a phaserre.
signal-only
Jelez majd tovább halad, megyezik a next utasítással.
signal-wait[-next]
Jelzi, hogy végzett az aktuális fázissal. Logikailag hiba ha a tevékenység előző phaser-művelete szintén signal volt, hiszen fölösleges kétszer jelezni, hogy az aktuális fázisbeli munkáját befejezte a tevékenység.
Egy signal és a következő next utasítás között lokális műveletek végezhetők, tehát a tevékenység már befejezte az aktuális fázist, de még nem akar a következőbe lépni. Az angol szakirodalom ezt a szinkronizációs módot nevezi fuzzy barriernek.

Redukálás

Maga a redukálás (reduce) nem a párhuzamossággal összefüggő fogalom, de a Habanero Java által nyújtott eszköz a szinkronizált párhuzamos tevékenységekből származó értékekkel dolgozik, így ebbe a fejezetbe került ennek ismertetése is.

A redukálást a funkcionális paradigmában járatosak a fold művelettel azonosíthatják.
Egyszerűen megfogalmazva: Adott valahány azonos típusú érték és egy rajtuk értelmezett bináris operátor, mely eredményének típusa azonos az értékek típusával. Így egy kezdő értékből kiindulva (pl. összeadásnál a 0, szorzásnál az 1) az adott értékek egymás után sorban össze-operálhatók a művelettel egyetlen értékké, amit redukált értéknek nevezünk.
Az értékek egy halmazt alkotnak, vagyis véletlenszerű, hogy milyen sorrendben fognak redukálásra kerülni. Ezért a redukálást csak kommuntatív és asszociatív művelettel végezhetjük. Ezáltal biztosítható, hogy adott érték-halmaz esetén az elemek elérésének sorrendjétől függetlenül mindig ugyanazt az eredményt kapjuk.

Accumulatorok

Az accumulator a phaserrel szinkronizált tevékenységekből származó értékeken elvégzett kommutatív, asszociatív művelet eredményének meghatározására szolgáló eszköz. A kényelmességen kívül hatékonyabb is a műveletet nem egy tevékenységben elvégezni, hanem azt is elosztani, erre szolgál az accumulator. A művelet eredményének meghatározását fázisonként végzi, vagyis minden fázisban az előző fázis végeredménye érhető el. Single statement használatakor a fázisátmenet során már befejezettnek tekintjük azt a fázist, amelyet éppen elhagyunk.

Az accumulatorok használatát megvilágítja a példakódok között található iteratív átlagolás példasorozat utolsó megoldása.

Accumulator létrehozása
accumulator a = accumulator.factory.accumulator(accumulator.Operator.SUM, int.class, ph); //operation, class, phaser

Az új accumulatorok létrehozását az accumulator.factory gyártó objektum accumulator metódusa végzi.
Első paraméterként az értékeken végzett műveletet kell megadni, ami az accumulator.Operator osztályban definiált értékek egyike lehet: SUM, PROD, MIN, MAX, ANY. Második paraméterként a feldolgozandó értékek típusát kell megadni, ami jeleneg az int és a double típusokhoz tartozó osztályok lehetnek. Végül meg kell adnunk egy phaser objektumot, melyhez a létrehozandó accumulator kötődni fog.
Csak azon tevékenységek használhatják az accumulatort, melyek a megadott phaserhez kapcsolódnak. Ha egy olyan tevékenység próbálja használni az accumulatort, mely nem kapcsolódik az accumulator létrehozásakor megadott phaserhez, akkor egy kivétel fog keletkezni futási időben.
Emellett az accumulator fel is veszi a megadott phaser szerkezetét, hogy a művelet elvégzését a phaserrel történő szinkronizációhoz hasonlóan elossza.

Érték küldése
a.send(datum);

Az accumulator létrehozásakor megadott típusú érték küldésekor az accumulator elvégzi a műveletet a tárolt értékkel.
Egy tevékenység adott fázisban többször is küldhet értéket ugyanannak az accumulatornak. Ekkor minden küldés külön hozzáadódik az eredményhez.

Eredmény kiolvasása
a.result(); //java.lang.Number

Az előző fázisban kialakult végeredményt adja meg java.lang.Number típusú objektumként.