Bevezető
A JoCaml rendszer az Objective Caml nyelv kísérleti kiterjesztése az elosztott join-kalkulus
programozási modellel. Ez a modell magasszintű kommunikációs és szinkronizációs csatornákat,
mobil ágenseket, leállás-detektálást és automatikus memória-kezelést tartalmaz. A JoCaml
segítségével nagy elosztott alkalmazások gyorsan fejleszthetők, a programozók kihasználhatják
mind az Objective Caml könnyű programozhatóságát és kiterjesztett könyvtárait, mind pedig a
join-kalkulus elosztott és párhuzamos jellemzőit.
A JoCaml tehát egy elosztott funkcionális nyelv, ahol a számítási nyelv az Objective Caml, a
vezérlő nyelv pedig a join-kalkulus.
A join-kalkulus egy kísérleti nyelv, ami a folyamat-algebrán
alapul. Egyszerű támogatást biztosít a párhuzamos és elosztott programozáshoz.
A join-kalkulus programozási modell jellemzői a több gépen futó párhuzamos folyamatok, a
statikus típusellenőrzés, a globális lexikális láthatóság, az átlátszó távoli kommunikáció, az
ágens-alapú mobilitás és bizonyosfajta hiba-detektálás.
A JoCaml nyelv tulajdonságai:
- Funkcionális nyelv: A függvényeknek kiemelt szerepe van a nyelvben, egy JoCaml
program alapvetően függvénydefiníciókból és függvényalkalmazásokból áll. A függvények
elsőrendű (first class) nyelvi elemek.
- Nem tisztán funkcionális: Tartalmaz módosítható adattípusokat is (például tömb,
referencia), melyeken értelmezve van az értékadás művelete. Ha a programunkban ilyen
típusú változókat használunk, akkor a hivatkozási átlátszóság sérülhet, megjelenhetnek a
mellékhatások. A nyelv tartalmaz más imperatív programozásra jellemző nyelvi elemeket
is, például ciklusokat.
- Mohó kiértékelési stratégiát használ.
- Erősen típusos: Minden értéknek, objektumnak, formális paraméternek és kifejezésnek a
típusa egyértelműen meghatározható. Ha valamit típus szerint rossz környezetben
használunk, akkor fordítási hibát kapunk (nincs implicit típuskényszerítés).
- Statikusan típusos: A típusok ellenőrzése (például a formális és aktuális paraméterek
kompatibilitásának ellenőrzése) fordítási időben történik. Végrehajtás közben semmilyen
ilyen jellegű ellenőrzés nincs, ami növeli a hatékonyságot.
- Van típuslevezetés: a programban általában nem kell megadni a típusokat, a fordító a
használatból ki tudja következtetni. A levezetés az típus-ellenőrzéssel együtt történik, még
fordítási időben.
- Használhatunk magasabbrendű függvényeket, és írhatunk – esetleg kölcsönösen –
rekurzív függvényeket is. A rendszer a többváltozós függvényeket a Curry módszer
szerint kezeli.
- A nyelvben Zermelo-Frankel halmazkifejezések nincsenek, de – mivel a nyelv mohó –,
ezen nincs is semmi meglepő, hiszen ezek a halmazkifejezések végtelen sok elemet is
tartalmazhatnak, amit mohó módon nem lehet kiértékelni.
- Van mintaillesztés.
- Nincs margó szabály.
- A nyelv támogatja a paraméteres polimorfizmust: írhatunk közös, általános kódú
függvényt különböző típusú adatstruktúrákhoz (van típusváltozó).
- A nyelv tartalmaz kivételkezelést.
- Paraméteres modul-rendszer (module system): lehet absztrakt adattípusokat létrehozni,
és funktorokat (modulokon értelmezett függvényeket) használni.
- Objektum orientált: Támogatja osztályok definiálását, objektumok létrehozását, az
öröklődést és az újrafelhasználást.
- Támogatja az elosztott és párhuzamos programozást, a kommunikáció és a
szinkronizáció csatornák használatával lehetséges. A párhuzamosan futó és együttműködő
komponensek nemcsak szinkronizálnak egymással és adatokat küldenek egymásnak, hanem
mobil kódrészleteket is továbbítanak.
- A nyelvben három végrehajtási mód érhető el:
- Interaktív környezet (top-level): minden beírt mondatot azonnal kiértékel, majd az
eredményt és annak típusát kiírja a képernyőre (read-eval-print ciklus); ez egy
kényelmes környezet mind a tanuláshoz, mind a programok gyors teszteléséhez és
hibakereséséhez.
- Bytecode fordító: a programot bytekódra fordíthatja le, amit egy C program
interpretál.
- Natív kód fordító.
Folyamatok
Fontos, hogy egy JoCaml program a deklaráción és a kifejezésen kívül folyamatokat (process) is
tartalmazhat. Ugyan a program alapvetően deklarációk és kifejezések sorozata, azonban mind a
definíciók, mind a kifejezések – bizonyos jól meghatározott körülmények között – tartalmazhatnak
folyamatokat. (Azonban a folyamat nem állhat „csak úgy magában”, tehát például a következő
program szintaktikailag nem helyes: deklaráció;; kifejezés1;; folyamat;; kifejezés2;;)
A folyamatok – a kifejezésekhez hasonlóan – egy számítást írnak le. Valójában a kifejezések és a
folyamatok nagyban hasonlítanak egymásra. Azonban a kiértékelésben alapvető különbségek
vannak:
- A rendszer a kifejezéseket szinkron módon értékeli ki: teljesen végrehajtja a kifejezés
kiértékelését, és csak utána lép tovább a következő kifejezésre, folyamatra vagy
deklarációra. A kifejezéseknek a kiértékelés végén mindig van valamilyen eredménye
(értéke).
- A folyamatok aszinkron módon hajtódnak végre: a rendszer elkezdi a folyamat
kiértékelését (egy külön szálon), de nem vár annak befejezésére, hanem egyből továbblép.
A rendszer tehát nem várja meg a kiértékelés végét, így a visszaadott értéknek sem lenne
sok értelme (mivel nem tudható előre, hogy a folyamat mikor fejeződik be). Éppen ezért a
folyamatok nem adnak (nem is adhatnak) vissza semmilyen értéket.
Megjegyzés: Már a kiértékelésből is látszik, hogy a folyamatok leginkább a program
párhuzamosításában hasznosak, egy szekvenciális programot minden további nélkül megírhatunk
folyamatok nélkül is. (Objective Caml-ben nem is voltak folyamtok, ez csak a join-kalkulussal
történő kibővítésénél került bele a nyelvbe.)
|
Kifejezés
|
Folyamat
|
Mit ír le?
|
A kifejezés egy számítás leírása.
|
A folyamat is egy számítás leírása.
|
Van-e visszaadott
érték?
Ha igen, akkor
mi?
|
Egy kifejezés minden esetben
visszaad egy értéket, mégpedig a
számítás eredményét.
|
A folyamatok sosem adnak vissza
semmilyen értéket, ha a számításnak mégis
lenne valamilyen eredménye, azt meg kell
„semmisíteni”.
|
Milyen a kiértékelés?
|
Szinkron: a rendszer teljesen
végrehajtja a kifejezés kiértékelését,
és csak azután lép tovább.
|
Aszinkron: a rendszer egy új szálon
elindítja a folyamat kiértékelését, majd
– mivel visszatérési érték nincs – nem vár a
befejezésére, hanem egyből megy tovább
(így a folyamat kiértékelése és a program
további része egymással párhuzamosan
futhat).
|
A folyamatok tehát a program párhuzamosításában játszanak nagy szerepet.
Párhuzamosan futó programoknál az egyik nagy kérdés mindig az, hogy a párhuzamos részek
hogyan kommunikálnak egymással.
- Egyrészt a folyamatok kommunikálhatnak egymással közös változók használatával,
ugyanis az azonosítók láthatási szabályai a folyamatok számára ugyanazok, mint a
kifejezések számára.
- A folyamatok ezen kívül csatornákon (más szóval port-neveken) keresztüli üzenetküldéssel
is tarthatják egymással a kapcsolatot.
Csatornák
A csatorna-deklaráció egyetlen önálló nyelvi szerkezetben deklarál egy portot (amely fogadja a
csatornára küldött üzeneteket) és egy ezeket figyelő folyamatot (őrzött folyamat). A folyamat
azért „őrzött”, mert a végrehajtása csak akkor kezdődik meg, amikor a csatornára üzenet érkezik – a
folyamat minden egyes üzenet érkezésekor végrehajtódik (kiértékelődik). Az üzenet több, vagy
akár nulla értékből állhat, azonban minden csatornánál már a deklaráláskor meg kell mondani, hogy
a csatorna hány és milyen típusú részekből álló üzenetet vár. A várt üzenetekhez formális paramétereket rendelünk, melyeket felhasználhatunk a csatorna őrzött folyamatában is, így a
csatorna különböző üzenetekre különbözőképpen reagálhat. Amikor a csatornára üzenet érkezik,
akkor a csatorna formális paraméterei felveszik az üzenetben kapott értéket, és az őrzött folyamat
végrehajtása megkezdődik. Mivel folyamatról van szó, így a végrehajtás
természetesen aszinkron lesz.
A háttérben minden csatornához tartozik egy várakozási sor, ahol a csatornára küldött, de még fel
nem dolgozott üzenetek várakoznak. A várakozási sornak csak a memória mérete szab határt.
A csatornáknak két nagy csoportja létezik: szinkron csatorna és aszinkron csatorna (a név a
csatornával történő kommunikáció típusára utal).
Aszinkron csatorna
Deklarálásának szintaxisa:
let def csatornanév! paraméter = őrzött_folyamat
Megjegyzés: azt, hogy egy csatornát deklarálunk a def kulcsszó jelzi, a csatorna aszinkron voltát
pedig a csatornanév után lévő „!” (ez a jelölés csak a csatorna-definícióban szerepel, a
csatornára történő hivatkozáskor nem kell kiírni)
Az aszinkron csatornák tulajdonságai:
- Mivel a folyamatok kiértékelése semmilyen értéket nem produkál, így nem meglepő, hogy
egy aszinkron csatornára jövő üzenetküldés sem eredményez semmilyen értéket.
- Az aszinkron csatornára történő üzenetküldés típusa aszinkron: a küldő elküldi az
üzenetet, amely a fogadónál egy üzenetsorba kerül. A küldő szál végrehajtása csak az
üzenet elküldésének idejére függesztődik fel, a küldő nem vár az őrzött folyamat
kiértékelésének befejezésére. A fogadó az üzenet feldolgozásakor kiveszi azt az
üzenetsorából és kiértékeli az őrzött folyamatot. Azonban – a hívó oldaláról – nem tudható
előre, hogy a kiértékelés pontosan mikor fog megtörténni.
- Tehát, ha üzenetet küldünk egy aszinkron csatornára, akkor a küldő csak elküldi az
üzenetet, de végrehajtása nem függesztődik fel az üzenet kiértékelésének idejére, hanem a
kiértékelés folyik tovább (esetleg az őrzött folyamat kiértékelésével párhuzamosan). Az
őrzött folyamat kiértékelése után a küldő semmilyen értéket nem kap vissza. Az előbbiekből
már látszik, hogy az aszinkron csatornára történő üzenetküldés maga is egy folyamat.
- Egy aszinkron csatorna típusa: [[paraméter1_típusa * … * paramétern_típusa]]
Példa:
Készítsünk egy olyan aszinkron csatornát, mely int típusú értékeket szállít, melyek értékét
tüzeléskor kiírja a konzolra:
# let def echo! x = print_int x;
# ;;
val echo : [[int]]
Megjegyzés: mivel a print_int i egy kifejezés, amely visszaadna egy értéket: (), ezért volt
szükséges hozzáfűzni egy „;”-t, ami ezt elnyomja. A „print_int x” ugyanis egy kifejezés, míg
a „print_int x;” egy folyamat.
Szinkron csatorna
Deklarálásának szintaxisa:
let def csatornanév paraméter = őrzött_folyamat
A szinkron csatornák tulajdonságai:
- Az aszinkron csatornákkal ellentétben a szinkron csatornának mindig kell, hogy legyen
visszatérési értéke. Mivel a szinkron csatorna egy folyamatot tartalmaz – amely nem
produkál semmilyen értéket – így jogos a kérdés, hogy vajon mi lesz a csatorna visszatérési
értéke? A visszatérési értéket az explicit reply [érték] folyamattal kell megadni. A
reply [érték] folyamatot tehát kötelező használni az őrzött_folyamat-ban (ha az
érték-t nem adjuk meg, akkor az „üres” reply egy ()-t ad vissza).
Megjegyzés: a visszaadott értéket úgy kell megadni, hogy minden végrehajtáskor pontosan
egy reply kerüljön kiértékelésre; ha az őrzött folyamat több reply-t tartalmaz (például:
if kifejezés then reply érték1 else reply érték2), akkor a visszaadott
értékek típusának meg kell egyeznie.
- A szinkron csatornára történő üzenetküldés típusa szinkron (randevú): a küldő elküldi az
üzenetet, majd a kiértékelése felfüggesztődik addig, amíg a szólított őrzött folyamat
kiértékelése el nem jut a reply [érték] folyamatig. Ez a folyamat adja meg ugyanis a
csatorna visszatérési értékét, a hívó rész további kiértékelése csak ezután folytatódhat. A
szinkron kommunikáció előnye, hogy itt az információ áramlása kétirányú: a hívó elküldi a
csatornának az üzenetet, majd a csatorna is küld egy értéket a hívónak.
- Tehát, ha üzenetet küldünk egy szinkron csatornára, akkor a küldő végrehajtása az
eredmény (ami szinkron csatornáknál kötelező) visszakapásáig felfüggesztődik, ezért az
őrzött folyamat és a program további részének kiértékelése nem történik egymással
párhuzamosan. Ezen okok miatt a szinkron csatornára történő üzenetküldés egy
kifejezés.
- Egy szinkron csatorna típusa: param1_típusa * … * paramn_típusa -> eredmény_típusa
Példa: a feladat ugyanaz, mint az előbb (egy int típusú értékeket szállító csatorna, amely
tüzeléskor kiírja az üzenet értékét a konzolra); azonban ez most szinkron csatorna lesz:
# let def echo_szinkron x = print_int x; reply
# ;;
val print : int -> unit
Megjegyzés: látható, hogy a csatorna – miután kiírta az üzenet aktuális értékét a konzolra – a
reply segítségével visszaad egy () értéket.
Szinkron csatornák és függvények
A szinkron csatorna leírásából – de legfőképpen a típusából – kiderül, hogy a szinkron csatornák
elég közeli rokonságban vannak a függvényekkel. Ha a szinkron csatornák és a függvények típusát
megnézzük, akkor rá kell jönnünk, hogy a két típus egy és ugyanaz (típus1 -> típus2). Ebből
következően mindenütt, ahol szinkron csatornák szerepelhetnek, ott szerepelhetnek függvények is,
és fordítva, ahol a fordító függvényt vár, ott lehet szinkron csatorna is (természetesen csak akkor,
ha a paraméter és az eredmény típusa is egyezik).
Bár a szinkron csatornák és a függvények típusa ugyanaz, azonban a két fogalom több dologban is
különbözik egymástól:
|
Függvény
|
Szinkron csatorna
|
Több paraméter kezelése:
|
A függvények a több paramétert a
Curry-módszer szerint kezelik: a
függvénynek igazából mindig csak egy
paramétere van (az első), eredményül
pedig egy (magasabbrendű) függvényt
kapunk, ami majd kezeli a többi
paramétert.
A függvény meghívásakor a paramétereket
szóközzel elválasztva adjuk meg.
Ha kevesebb paramétert adunk meg,
akkor nem egy értéket (a függvény
visszatérési értékét) kapunk, hanem egy
magasabbrendű függvényt, amelynek
értéke a meg nem adott paraméterektől
függ.
|
A szinkron csatornáknak is igazából mindig
csak egy paramétere van. Ha több
paramétert szeretnénk, akkor azt egy
összetett adatszerkezetben (konkrétan
rendezett n-esben) kell megadni (ez jól
látszik abból, hogy ha több paraméter van,
akkor azokat kerek zárójelben vesszővel
elválasztva kell megadni – pont úgy,
ahogyan a rendezett n-eseket).
A csatornára történő üzenetküldéskor a
paramétereket (mivel az egy rendezett n-es)
szintén kerek zárójelben, vesszővel
elválasztva kell megadni.
Ha a csatorna használatakor kevesebb
paramétert adunk meg, akkor szintaktikus
hibát kapunk.
Megjegyzés: ez a paraméterkezelés nem
csak a szinkron csatorna sajátossága: az
aszinkron csatornák is pontosan így kezelik
az egynél több paramétert.
|
Érték visszaadása:
|
Az érték visszaadása a függvényeknél
implicit módon történik: a függvény
visszatérési értéke automatikusan a
függvénytörzs (ami egy kifejezés) értéke
lesz.
|
A szinkron csatornáknál az érték
visszaadása explicit: a törzsben kötelez a
reply érték szerkezet használata.
|
Csatornák használata
Üzenetküldés csatornára:
csatornanév aktuális_paraméter
A paraméter megadása kötelező, ami mindig egy – a csatorna definíciójában meghatározott számú
és típusú elemekből álló – rendezett n-es.
Mint arról korábban szó volt: az aszinkron csatornára történő üzenetküldés egy folyamat, míg a
szinkron csatornára történő üzenetküldés egy kifejezés.
Folyamatok használata
Már több szó esett a folyamatokról (például a csatorna deklarálásakor egy folyamatot kell megadni,
az aszinkron csatorna meghívása maga is egy folyamat, létre tudunk hozni folyamatokból álló
szekvenciát és elágazást, stb.), azonban arról még nem, hogy ezeket hogyan lehet a
programban használni. A program ugyanis alapvetően deklarációk és kifejezések listája, így ha
„csak úgy” beleírnánk a programba, hogy például „echo 42”, akkor szintaktikus hibát kapnánk
(hiszen ez se nem deklaráció, se nem kifejezés; ez egy folyamat). A folyamatok – a kifejezésekkel
ellentétben – valójában nagyon korlátozott formában fordulhatnak csak elő a programban: például
nem szerepelhetnek kötés jobb oldalán, nem adhatók át paraméterként (sem függvénynek, sem
csatornának) stb.
A folyamatot ezért valamilyen módon „át kell alakítani” („be kell csomagolni”) egy kifejezésbe, ha
ki szeretnénk értékeltetni.
A spawn folyamatpéldányból kifejezést állít elő, melynek kiértékelésekor egy konkurens folyamat
indul el. A spawn { folyamat } tehát már egy kifejezés, amely elindítja a folyamat
kiértékelését, majd azonnal visszatér egy () értékkel. A folyamat kiértékelése a program további
részének kiértékelésével párhuzamosan történik.
Nézzük a következő programot:
spawn { echo 1 } ;;
spawn { echo 2 } ;;
A programban mind a két spawn csak elindította a folyamatok kiértékelését, így az echo 1 és az
echo 2 folyamatok kiértékelése egymással párhuzamosan történik. Ezért előre nem tudható, hogy
a fenti program 12-t, vagy 21-t fog kiírni.
A csatornák tulajdonságai
Párhuzamos végrehajtást a párhuzamos kompozíció operátorral ( „|” ) is el lehet érni. Ez egy
bináris infix operátor, amely a megadott két folyamatot egymással párhuzamosan értékeli ki.
Szintaxis:
folyamat1 | folyamat2 (eredményül egy folyamatot kapunk)
A következő példa szemantikailag ekvivalens az előzővel:
spawn { echo 1 | echo 2 } ;;
A párhuzamos kompozíció operátor tulajdonságai:
- Kommutatív: P | Q = Q | P
- Asszociatív: {P | Q} | R = P | {Q | R} = P | Q | R (emlékezzünk rá, hogy a
folyamatokat – a kifejezésekkel ellentétben – nem kerek, hanem kapcsos zárójel
használatával lehet csoportosítani)
- A szinkron és az aszinkron csatornák típusa különböző. Ha rossz környezetben használjuk
őket, akkor hibaüzenetet kapunk:
# spawn { echo_szinkron 1 } ;;
Characters 7-14:
Expecting an asynchronous channel, but receive int -> unit
- Mivel a csatornák elsőleges (first class) elemek, így küldhetők üzenetként egy másik csatornára
(szinkronra és aszinkronra is), és a szinkron csatornáknál visszatérési értékként is használhatóak.
Az olyan csatornát, amelynek (valamelyik) paramétere és / vagy értéke is csatorna
magasabb-rendű csatornának hívjuk.
A következő példa egy magasabb-rendű (szinkron) csatornát (twice) definiál, melynek
paramétere és visszatérési értéke is csatorna: a paramétere egy egy-változós egy-értékű szinkron
csatorna (vagy egy egy-változós egy-értékű függvény), visszatérési értéke pedig egy ugyanilyen
típusú csatorna, amely azonban az üzeneteire kétszer is végre fogja hajtani a műveletet. (Fontos,
hogy a bemeneti paraméter olyan csatorna vagy függvény legyen, amelynél a paraméter és
eredmény ugyanolyan típusú.)
# let def twice f =
# let def r x = reply f (f x) in
# reply r
# ;;
val twice : ('a -> 'a) -> 'a -> 'a
- Az előző példán látszik, hogy a csatornák is – mint szinte minden más típus – polimorfak is
lehetnek (tehát tartalmazhatnak típusváltozót – ugyanis JoCaml-ben csak paraméteres
polimorfizmus van). A következő példán látható, hogy 'a típusváltozó egyszer int, máskor
string:
# let def succ x = reply x+1 ;;
# let def double s = reply s^s ;;
#
# let f = twice succ in
# let g = twice double in
# print_int (f 0) ; print_string (g "X") ;;
val succ : int -> int
val double : string -> string
-> 2XXXX
- Érték korlátozás (value restriction): gyenge típusváltozó ('_a) használata. Az ilyen
típusváltozó csak egyszer testesítődik meg (első használatkor eldől a tényleges típusa, utána már
más típussal nem lehet használni).Például nézzük a következő példát:
Definiálunk egy olyan csatornát, ami egy adott aszinkron
port-nevet hív meg egy adott paraméterrel; a csatorna folyamata csak akkor értékelhető ki, ha mind
a port-név, mind a paraméter adott.
# let def port! p | arg! x = p x ;;
val arg : <<'_a>>
val port : <<<<'_a>>>>
Az őrzött folyamatból látszik, hogy a p egy tetszőleges paraméterű aszinkron csatorna és az x is
tetszőleges típusú. Azonban a p paraméterének típusa az x típusával egyező kell, hogy legyen
(ugyanis az x üzenetet küldjük el a p csatornára). Ezért a p és az x nem lehetnek tetszőleges
típusúak, mert ez esetben a spawn { port echo | arg "szoveg"} is helyes lenne, ami
nyilvánvalóan típushibás. (Ha mindkét típusban 'a típusváltozó szerepelne, akkor ez minden
használatkor más típust vehetne fel, például az port echo folyamatban int lenne, míg az
arg "szoveg" folyamatban string.)
Éppen ezért, ha vagy a p, vagy az x típusa eldől, akkor ezt a típust le kell rögzíteni (az '_a gyenge
típusváltozó minden előfordulásában felveszi az adott típust), így a másik résztvevő típusa is
biztosítottan helyes típus lesz.
Csatornák esetében gyenge típusváltozót akkor kapunk, amikor az egy definícióban szereplő
port-nevek osztoznak a típusváltozón.
Kivételkezelés párhuzamos program esetében
A kifejezések kivételkezelése a try … kifejezés írásával lehetséges (folyamatokhoz nem kapcsolhatunk try-t). Ha a kivételt nem kezeljük le (vagy azért mert nem is tartozott hozzá try, vagy azért, mert ugyan volt try, de a kivételt nem tudta lekezelni),
akkor a viselkedés attól függ, hogy a hívó vár-e a kiértékelés eredményére.
Megjegyzés: a folyamatok kiértékelésére a hívó sosem vár, de elképzelhető, hogy egy kifejezés
kiértékelésére sem vár, például abban az esetben, ha a kifejezés egy folyamat része (pl. a
kifejezés;folyamat szekvencia egy folyamat, mert a szekvencia utolsó tagja folyamat – ebben
az esetben az összetett folyamat kiértékelésére a hívó nem vár, de a kiértékelés magában foglalja a
kifejezés kiértékelését is).
- Aszinkron szálak kivételkezelése (a hívó nem vár a kiértékelés eredményére): a kivételt
nem lehet elkapni, a rendszer kiírja a kivételt a standard output-ra, majd az aszinkron szál
terminál. A kivétel semmilyen más szálat nem érint.
Példa:
# spawn {
# { failwith "Kivetel"; print_string "Bye";}
# | { for i = 1 to 10 do print_string "-" done; }
# } ;;
-> Uncaught exception: Failure("Kivetel")
-> - - - - - - - - - -
A példában a „failwith "Kivetel"; print_string "Bye";” és a „for i = 1 to 10
do print_string "-" done;” folyamatok egymással párhuzamosan futnak. Az első folyamat
váltja ki a kivételt, melynek hatására ennek a résznek a kiértékelése abbamarad (látható, hogy a
Bye már nem íródik ki), azonban ez a kivétel a többi párhuzamosan futó részt „nem zavarja” (a
másik szál így gond nélkül ki tudja írni a kötőjeleit).
- Szinkron szálak kivételkezelése (a hívó vár a kiértékelés eredményére – ez csak kifejezés
esetén lehetséges; de mint láttuk kifejezés esetén sincs mindig így): ha a kivételt nem kezeljük le,
akkor a kivételt a hívó fogja megkapni (az, hogy utána mit kezd a kivétellel, az már csak rajta
múlik).
Példa:
# let def die () = failwith "die"; reply ;;
#
# try
# die ()
# with _ -> print_string "dead\n" ;;
val die : unit -> 'a
-> dead
A failwith "die"; reply folyamat kiértékelésére a die szinkron csatornára üzenetet küldő
kifejezés vár, így ő kapja a kivételt, amelyet le is kezel (kiírja, hogy dead).
- Egy egy rész kiértékelésére akár többen is várhatnak.Mi történik
ebben az esetben, ki kapja a kivételt? A válasz: mindenki. Tehát, ha a számítás eredményére többen
is várakoznak, akkor a kivétel többszöröződik, és az összes – a kivételt kiváltó rész kiértékelésére
váró – szálra dobódik.
Példa:
# let def a () | b () = failwith "die"; reply to a | reply to b ;;
#
# spawn {
# { (try a () with _ -> print_string "hello a\n"); }
# | { (try b () with _ -> print_string "hello b\n"); }
# } ;;
val b : unit -> unit
val a : unit -> unit
-> hello a
-> hello b
Látszik, hogy a kivételt mind az a, mind a b csatornára üzenetet küldő kifejezés megkapta (és
mindkettő le is kezelte).
Felelősök
- A nyelvleírást készítette: Szabadi Tamás 2007. 01. 14.
- A nyelvleírást az ML nyelvcsalád többi tagjáról készült leírásokhoz
illesztette: Lázár János 2009. 07. 03.