Az ERLANG programozási nyelv

Párhuzamosság

A processz és a processzek közti kommunikáció alapvető fontosságú fogalmak az Erlangban.

Processz létrehozása

A processz egy önálló számítási egység, amely más processzekkel párhuzamosan létezik a rendszerben. Processz létrehozása a spawn/3 paranccsal történik:

Pid = spawn(Modul, Fuggvenynev, Argumentum_lista)

Az új processz az alábbi függvényhívást kezdi el végrehajtani:

Modul:Fuggvenynev(Arg1,Arg2,...ArgN)

ahol Arg1,Arg2,...,ArgN az "Argumentum_lista" lista elemei. (Az "Argumentum_lista" lista üres is lehet). Ahhoz, hogy a spawn függvény sikeresen lefusson, biztosítanunk kell a felhasználandó modul láthatóságát. Például Eshell használata esetén előbb a c(Modul) parancsot kell kiadnunk, hogy a shell lefordítsa az adott modult.
Gondoskodnunk kell továbbá arról, hogy a függvényhívás a függvény aritásának megfelelően történjen. Tehát ügyelnünk kell arra, hogy az argumentumok listájában megadott elemeknek a száma igazodjon a hívandó függvény argumentumainak a számához.

A függvény visszatérési értéke az újonnan kreált processz azonosítója, mely szükséges a processzel való mindenféle kommunikáció során. A spawn függvényhívás azonnal visszatér, amint a processz létrejött, nem várja meg a függvény kiértékelését. A processz automatikusan terminál, amint a spawn paraméterben átadott függvény kiértékelése befejeződik. A függvény visszatérési értéke nyilván elveszik.

Processzek kommunikációja

Az Erlangban a processzek közti kommunikáció egyetlen formája az üzenetküldés. Ehhez a ’!’ primitív biztosított:

Expr1 ! Expr2

Ez a kifejezés az Expr2 értékét küldi el az Expr1 által azonosított folyamat számára. Az Expr2 értéke egyben a Expr1 ! Expr2 kifejezés visszatérési értéke is lesz. Expr1 érétékének egy processz azonosítónak, egy regisztrált névnek (atomnak) vagy egy {Name,Node} párnak kell lennie. Itt a Name egy atom, Node pedig egy csúcs, amely szintén egy atom. Az üzenetküldés aszinkron módon történik.

Üzenetek fogadása

A receive primitív szolgál üzenetfogadásra. Szintaxisa:

receive Pattern1 [when GuardSeq1] -> Body1; ...; PatternN [when GuardSeqN] -> BodyN end

Az üzeneteket a fogadó a küldés sorrendjében kapja meg. Minden processznek vagy egy üzenetfogadó puffere (mailbox), az üzenetek itt tárolódnak feldolgozásukig. A fentiekben Pattern1,...,PatternN minták, melyekhez a mailboxbeli üzenetek kerülnek illesztésre (az illesztés a beérkezés sorrendjében történő szekvenciális keresés). Ha van illeszkedő üzenet és a megfelelő őrfeltétel teljesül, az üzenet kiválasztódik, kikerül a mailboxból, és a megfelelő BodyI kiértékelődik. A receive az utoljára kiértékelt BodyJ–beli kifejezés értékét veszi fel.

A receive szerkezet egy blokkoló szerkezet. Ha a receive szerkezetre kerül a vezérlés, akkor a receive mindaddig blokkol (meghatározhatatlan ideig), amíg egy olyan üzenet érkezik a folyamat számára, amely egyezik a receive szerkezetben felsorolt minták egyikével és egyben az adott mintához tartozó őrfeltételt is kielégíti.

Egyszerű kommunikációs példa

Ez a példa az Erlang honlapján található "Concurrent Programming" aloldaláról származik (link). Tegyük fel, hogy adott két folyamat, ezek legyenek A és B . Az A folyamat futása során az alábbi utasítás hajtódik végre:

B ! {self(),{digits,[1,2,6]}}

Itt a self/0 függvény az A folyamat processz azonosítójával fog visszatérni (bővebben erről a "Processz azonosító küldése üzenetként" részben olvashatunk). Így az elküldött üzenet a {A,{digits,[1,2,6]}} formát fogja ölteni.
Tegyük fel, hogy a B folyamat egy olyan függvényt hajt végre, amelyben az alábbi kódrészlet található:

receive {A,{digits,D}} -> analyse(D); end

Itt egy szelektív üzenetfogadás történik (bővebben lásd a "Szelektív üzenetfogadás" részt). A receive primitívben csak az A folyamattól érkező, {digits,D} formájú üzenetek feldolgozása történik meg.

Amikor a B folyamat megkapja az A folyamat által küldött üzenetet, akkor az illesztésre kerül a receive primitívben található egyetlen mintához. Mivel az illesztés sikeres, ezért a B folyamat keretein belül végrehajtódik az analyse(D) függvény, amely a D-ben fogadott számjegyek feldolgozására szolgál.

Szelektív üzenetfogadás

Tegyük fel, hogy van három folyamatunk: A,B és C, továbbá, hogy A az msg1 üzenetet küldi C-nek és B pedig a msg2 üzenetet. C-ben az üzenetfeldolgozó rész a következő módon néz ki:

receive msg1 -> true end, receive msg2 -> true end

Ekkor C a msg1 üzenetet fogja először feldolgozni, aztán a msg2 üzenetet, függetlenül az üzenetek elküldésének a sorrendjétől.

Bármely üzenet elfogadása

Tegyük fel, hogy az egyik folyamatunkban az alábbi üzenetfeldolgozási rész található:

receive Msg -> ... ; end

Ekkor bármely beérkező üzenetre a Msg mintához tartozó törzs fog végrehajtódni (Fontos, hogy amíg msg egy atom, addig Msg egy változó. Amíg a Szelektív üzenetfogadás részben jól meghatározott atomok fogadására voltunk felkészülve addig itt a változó segítségével tetszőleges atomot feldolgozhatunk egyetlen receive ágban). Ekkor fontos, hogy milyen sorrendben érkeztek az üzenetek a folyamat pufferébe. Ezt a technikát használhatjuk például üzenetek "elnyelésére" is, mint ahogy arról lentebb az Időtúllépés rész példái között olvashatunk.

Processz azonosító küldése üzenetként

Tetszőleges folyamat végrehajtása közben az adott folyamat azonosítóját megkaphatjuk a self/0 függvény segítségével és így lehetőségünk nyílik azt más folyamatok számára elküldeni. Az üzenetek fogadói így tudni fogják, hogy mely folyamattól kaptak üzenetet.

Tegyük fel, hogy van három folyamatunk, ezek legyenek A,B és C. Az alábbi példa azt fogja szemléltetni, hogy milyen módon viselkedhet egy folyamat üzenet továbbitóként. Az A folyamat az alábbi üzenetküldést intézi:

B ! {msg1,self()}

Tehát az A folyamat a msg1 üzenetet és a saját folyamt-azonosítóját küldi el a B folyamat számára. A B folyamat üzenetfeldolgozó része az alábbi módon néz ki:

receive {msg1,A} -> C ! {msg1,A} end

Így a B folyamat az A folyamattól kapott üzenetet rögtön továbbítja a C folyamat számára, az A folyamat folyamat-azonosítójával egyetemben. Végül a C folyamat a következőképpen dolgozza fel az üzenetet:

receive {msg1,A} -> A ! msg2 end

Példa a folyamatok közötti kommunikációra

Az alábbiakban egy egyszerű (ugynakkor szemléletes) példaprogramot láthatunk, amely a folyamatok létrehozását, és a folyamatok közötti kommunikációt szemlélteti. Ez a példa az Erlang honlapján található "Concurrent Programming" aloldaláról származik (link).

-module(echo). -export([go/0, loop/0]). go() -> Pid2 = spawn(echo, loop, []), Pid2 ! {self(), hello}, receive {Pid2, Msg} -> io:format("P1 ~w~n",[Msg]) end, Pid2 ! stop. loop() -> receive {From, Msg} -> From ! {self(), Msg}, loop(); stop -> true end.

A go/0 függvényt végrehajtó folyamat a loop/0 függvény külön folyamatként való elindítását végzi el első lépéként, majd egy üzenetküldéssel folytatja az imént elindított folyamat számára. Az üzenetben elküldi a saját folyamat-azonosítóját, és a hello atomot. Mivel a receive szerkezet egy blokkoló szerkezet, ezért ezen a ponton a go/0 függvényt végrehajtó folyamat blokkolt állapotba kerül.

A loop/0 függvény üzenetfeldolgozási része két részre osztható: az első részben {From, Msg} formájúak üzenetek feldolgozása játszódik le. Ha ilyen üzenetet kap a folyamat, amely a loop/0 függvény végrehajtását végzi, akkor a Msg üzenetet visszaküldi a saját folyamat-azonosítójával egyetemben a feladónak. A további üzenetek fogadásáról a rekurzív függvényhívás gondoskodik. A második részben a stop atom fogadása történik meg. Ha ilyen üzenetet kap a folyamat, amely a loop/0 függvény végrehajtását végzi, akkor a szóban forgó folyamat befejezi a működését.

Miután a loop/0 függvényt végrehajtó folyamat visszaküldte a go/0 függvényt végrehajtó folyamatnak a tőle kapott üzenetet, az utóbbi a kimenetre írja a "P1 hello\n" stringet, majd egy stop atomot küld a loop/0 függvényt végrehajtó folyamatnak, amely ennek hatására befejezi működését. Ezután a go/0 függvényt végrehajtó folyamatnak is végetér a működése.

Összetettebb példa

Az alábbi példaprogram a pi matematikai konstans értékét közelíti a getPI/2 függvény segítségével. A használt módszer menete alapvetően a következő: választunk egy N természetes számot, és generálunk N darab (x,y) párt, ahol x és y a (0,1) intervallumból veszik fel az értékeiket egyenletesen.
Legyen n azon (x,y) párok darabszáma, amelyeknek a távolsága az origótól kisebb vagy egyenlő, mint 1. Ekkor pi közelítőleg 4*n/N.

A példaprogram ennek a módszernek egy párhuzamosított változatát alkalmazza. A getPI függvény első argumentumában (NProc) kell megadnunk, hogy hány folyamattal kívánjuk a számítást elvégezni. Második argumentumaként (NPoints) azt kell megadnunk, hogy hány párt szeretnénk generáltatni az egyes folyamatokkal.

-module(pi). -export([getPI/2, master/2, ant/2, generate/1]). getPI(NProc,NPoints) -> GoodPoints = master(NProc,NPoints), io:format("~.8g~n",[4.0 * (GoodPoints / (NProc * NPoints))]). master(NProc,NPoints) -> case NProc of 0 -> 0; _ -> Pid = spawn(pi,ant,[self(),NPoints]), Tmp = master(NProc - 1,NPoints), receive {Pid,Generated} -> Tmp + Generated end end. ant(Pid,NPoints) -> random:seed(now()), Points = generate(NPoints), Pid ! {self(), Points}. generate(NPoints) -> case NPoints of 0 -> 0; _ -> X = random:uniform(), Y = random:uniform(), D = math:sqrt(X*X + Y*Y), if D =< 1.0 -> 1 + generate(NPoints - 1); true -> generate(NPoints - 1) end end.

A generate/1 függvény az NPoints argumentum alapján a következőket teszi: ha NPoints egyenlő nullával, akkor 0 értékkel tér vissza, különben végrehajtja egy "véletlen" pontnak a generálásást, és vizsgálatát.
A véletlen pont generálásának a menete a következő: X és Y értéke a random modulban található uniform függvény segítségével kapnak értéket. Ennek a függvénynek egy, a 0.0 és 1.0 intervallumból egyenletes eloszlással származó lebegőpontos szám a visszatérési értéke.
A D változó értéke az X és Y változó által meghatározott pont origótól mért távolsága lesz. Ha D kisebb vagy egyenlő mint 1.0, akkor a generate/1 függvény visszatérési értéke 1 és a generate/1 függvény rekurzív hívásának a visszatérési értékének az összege lesz, ahol a rekurzív hívás argumentuma NPoints - 1. Ha D nagyobb mint 1.0, akkor a függvény visszatérési értéke a generate/1 függvény rekurzív hívásának visszatérési értéke lesz, az előbbi argumentummal.
Így a generate/1 függvény egy véletlen pont generálását és vizsgálatát NPoints alkalommal fogja végrehajtani.

ant(Pid,NPoints) -> random:seed(now()), Points = generate(NPoints), Pid ! {self(), Points}.

Az ant/2 függvény először inicializálja a random modul függvényeit a jelenlegi idővel, amelyet a now/0 függvény meghívásával kap meg. Ezután meghívja a generate/1 függvényt, melynek eredményét a Points változóban tárolja el. Az ant/2 függvényt végrehajtó folyamat végül egy üzenetküldést intéz az ant/2 függvény argumentumaként kapott Pid processz azonosítóval rendelkező folyamat számára. Ebben az üzenetben az ant/2 függvényt végrehajtó folyamat elküldi a saját folyamat azonosítóját és a Points változó értékét.

master(NProc,NPoints) -> case NProc of 0 -> 0; _ -> Pid = spawn(pi,ant,[self(),NPoints]), Tmp = master(NProc - 1,NPoints), receive {Pid,Generated} -> Tmp + Generated end end.

A master/2 függvény hasonlóan a generate/1 függvényhez az NProc argumentuma alapján a következőt teszi: ha NProc egyenlő nullával, akkor a visszatérési értéke nulla lesz, különben végrehajtja egy ant/2 függvényt végrehajtó folyamatnak az elindítását.
Ha NProc nem nulla, akkor a spawn/3 függvény meghívásával létrejön egy folyamat, amely az ant/2 függvényt hajtja végre a self() és NPoints argumentumokkal. Így az újonnan létrehozott folyamat a master/2 függvény számára fogja elküldeni az általa végrehajtott generate/1 függvény eredményét. Ennek az új folyamatnak a folyamat azonosítóját a függvény a Pid változóban tárolja el.
A Tmp változó értéke a master/2 függvény visszatérési értéke lesz, ahol a master/2 függvény argumentumai NProc - 1 és NPoints lesznek.
Végül a master/2 függvényt végrehajtó folyamat belép egy receive vezérlési szerekezetbe, ahol is blokkolt állapotba kerül, és egészen addig blokkolt marad, amíg a legutóbb elindított Pid folyamat azonosítóval rendelkező folyamattól nem kap egy megfelelő {Pid,Generated} üzenetet. Ekkor a master/2 függvény visszatérési értéke Tmp értékének és Generated értékének az összege lesz.
Így a master/2 függvényt végrehajtó folyamat visszatérési értéke az összes általa elindított, ant/2 függvényt végrehajtó folyamatok visszatérési értékeinek az összege lesz.

getPI(NProc,NPoints) -> GoodPoints = master(NProc,NPoints), io:format("~.8g~n",[4.0 * (GoodPoints / (NProc * NPoints))]).

A getPI/2 függvény feladata az, hogy meghívja a megfelelő argumentumokkal a master/2 függvényt, majd a függvény visszatérési értékéből kiszámítsa a pi matematikai konstans közelítését, végül a kapott eredményt formázottan a kimenetre írja.

Összetettebb példa 2: Általános párhuzamosító alkalmazás

Az alkalmazások implementálása során lehetnek olyan esetek, mikor sokáig tartó taskokat (feladatokat megvalósító programokat/ programrészleteket) kell N-szer lefuttatni. Például: állítsunk össze és küldjünk ki egy emailt 100-szor. Ha feltesszük, hogy a taskok egymástól függetlenek és párhuzamosíthatóak, akkor az alkalmazásunk futási ideje nagyban csökkenthető, ha a taskok párhuzamosan vannak végrehajtva.

A fenti problémára egy általános "demo" alkalmazás megtalálható és letölthető a példaprogramok között.

Processzek regisztrálása

Egy folyamat címzésére nem csak a processz azonosítóját használhatjuk fel, hanem egy esetlegesen hozzá rendelt atomot is. Erre a feladatra az alábbi register/2 beépített függvényt használhatjuk fel:

register(Name, Pid)

Ez a függvény a Name argumentumban megadott atomot rendeli hozzá névként a Pid processz azonosítóval rendelkező folyamathoz. Ha egy névvel ellátott folyamat terminál, akkor a neve automatikusan kikerül a regisztrált nevek közül.

Két további beépített függvény is rendelkezésünkre áll:

Példa processz regisztrálásra

Az alábbi processz regisztrálási példa az Erlang honlapján található "Concurrent Programming" aloldaláról származik (link).

start() -> Pid = spawn(num_anal, server, []) register(analyser, Pid). analyse(Seq) -> analyser ! {self(),{analyse,Seq}}, receive {analysis_result,R} -> R end.

A start függvény első lépésként egy server függvényt indít el (a num_anal csomagból) különálló folyamatként. Második lépésként beregisztrálja ezt a frissen indított processzt az analyser atommal.

Az analyse függvényt végrehajtó folyamat első lépésben a regisztrált analyser folyamathoz küldi el a saját processz azonosítójával az analyse atomot és a Seq változót, amelynek értékét argumentumként kapja meg.
Ezután a függvényt végrehajtó folyamat blokkolt állapotba kerül a receive primitív miatt, egészen addig, amíg az {analysis_result,R} mintára illeszkedő üzenetet nem kap. Ha ez megtörténik, akkor a visszatérési érték az R értéke lesz.

Időtúllépés (timeout)

A receive primitív kibővíthető egy Timeout ággal:

receive Pattern1 [when Guard1] -> Body1; Pattern2 [when Guard2] -> Body2; ... after TimeoutExpr -> BodyT end

A TimeoutExpr kifejezésnek egy integer-ré kell kiértékelődnie (vagy egy speciális atommá, erről lentebb olvashatunk), ez az érték lesz a várakozás időtartama milliszekundumokban mérve. Ha ezen idő letelte alatt egy üzenet sem választódik ki, időtúllépés következik be, és az BodyT értékelődik ki. Ebben az esetben a BodyT kifejezés visszatérési értéke lesz a receive...after kifejezés visszatérési értéke.

Két speciális eset fordulhat elő a TimeoutExpr kifejezés értékére vonatkozóan:

Lehetőségünk van a receive primitívet a következő módon használni:

receive after TimeoutExpr -> BodyT end

Ekkor egyetlen üzenet sem kerül feldolgozásra, csak a meghatározott (TimeoutExpr kifejezés értékének megfelelő) idő után a BodyT kerül végrehajtársa. Ezen konstrukció segítségével készíthetünk például időzítőket (lásd: lentebbi példák).

Példák időtúllépésre

Az alábbi példák az Erlang honlapján található "Concurrent Programming" aloldaláról származnak (link).

sleep

Az "alvást" megvalósító kódrészlet a következő:

sleep(T) -> receive after T -> true end.

Ebben a példában a sleep/1 függvény receive primitívében nincs üzenetfeldolgozást végző kódrészlet, így az ezt a függvényt végrehajtó folyamat összes ("alvás" alatt kapott) üzenete az üzenetfogadó pufferben marad. A folyamat a beérkező üzenetek figyelmen kívül hagyását T ideig végzi.

suspend

Az alábbi függvény egy adott folyamat határozatlan ideig történő felfüggesztését valósítja meg:

suspend() -> receive after infinity -> true end.

alarm

Az alábbi kódrészlet a timer modul részét képezi, és azért felelős, hogy egy folyamat saját maga számára állítson be időzített "figyelmeztetést":

set(Pid, T, Alarm) -> receive after T -> Pid ! Alarm end. set_alarm(T, What) -> spawn(timer, set, [self(),T,What]).

A set/3 függvény a sleep/1 függvényhez hasonló szerkezetű, azonban T időnyi várakozás után a függvényt végrehajtó folyamat a Pid processz azonosítóval rendelkező folyamat számára küldi el az argumentumként kapott Alarm értékét.

A set_alarm/2 függvény egy olyan folyamatot indít el, amely a set/3 függvényt fogja végrehajtani a self(), T és What paraméterekkel. Az így elindított foylamat tehát a set_alarm/2 függvényt végrehajtó folyamat számára fogja T idő múlva fogja elküldeni a What értékét, ezzel valósítva meg az időzített "figyelmeztetést".

flush

A végrehajtó folyamat üzenetfogadó pufferének kiürítését az alábbi programrészlettel lehet megtenni:

flush() -> receive Any -> flush() after 0 -> true end.

A flush/0 függvény receive ágában a beérkező üzenetek az Any változóval kerülnek mintaillesztésre, amely minden esetben sikeres lesz (mivel Any egy változó). Sikeres mintaillesztés esetén az illesztett üzenet kikerül a folyamat üzenetfogadó pufferéből, így jelen esetben a flush/0 függvény rekurzív hívásával az üzenet elvész. Miután a végrehajtó folyamat üzenetfogadó pufferéből elfogytak az üzenetek, az after ág gondoskodik róla, hogy a vezértlés kikerüljön az amúgy blokkoló receive primitívből.

Elosztott programozás Erlangban

Egy elosztott erlang rendszer tartalmaz több futásidejű rendszert, amelyek, ahogy a nevükben is benne van, futás időben kommunikálnak egymással. Minden ilyen rendszert node-nak nevezünk. A különböző node-ok folyamatai közötti üzenetek, a linkek és monitorok transzparensek, ha a folyamat azonosítót (pid) használják. A folyamatok regisztrált nevei azonban lokálisak minden node-on.

Node-ok

A node (csomópont) egy végrehajtható futásidejű erlang rendszer, amely kapott egy nevet a parancssori indítás során. A névadás történthez a –name és a –sname kapcsolóval. A kettő közötti különbség, hogy az előbbi egy hosszú nevet, míg utóbbi egy rövid nevet definiál. A formátum a node neve egy atom name@host, ahol a név a felhasználó által megadott és a szerver a teljes host neve, ha hosszú neveket használnak, vagy az első része a host nevének, ha rövid neveket használnak. A node() visszaadja a csomópont nevét.

% erl -name dilbert (dilbert@uab.ericsson.se)1> node(). 'dilbert@uab.ericsson.se' % erl -sname dilbert (dilbert@uab)1> node(). dilbert@uab

Fontos, hogy egy hosszú névvel ellátott node nem tud kommunikálni egy rövid nevű node-al.

Node kapcsolatok

Elosztott erlangban a node-ok lazán kapcsolódnak. Kapcsolatok alapértelmezés szerint tranzitívak. Ha az A node csatlakozik a B node-hoz, és B kapcsolódik a C node-hoz, akkor az A node is próbál csatlakozni a C node-hoz. Ez a funkció kikapcsolható, a parancssor segítségével a connect_all kapcsoló hamisra állításával. Ha egy node leáll, minden kapcsolata törlődik. A disconnect_node(Node) erlang hívás arra fogja kényszeríteni a node-ot, hogy szétkapcsoljon. A nodes() függvény visszatérési értéke pedig egy lista , ami tartalmazza a node-hoz jelenleg kapcsolódó más node-okat.

Rejtett node-ok

Elosztott erlangban néha hasznos lehet, hogy úgy csatlakozzunk egy node-hoz, hogy közben nem létesítünk kapcsolatot az összes többi node-al. Erre a célra használható a rejtett(hidden) node. Egy ilyen node-ot parancssorból a –hidden kapcsolóval indíthatunk. A rejtett node-ok és más node-ok közötti kapcsolatok nem tranzitívak, ezeket expliciten kell beállítani. Ezen kívül a nodes() függvény által visszaadott listában a rejtett node-ok nem szerepelnek. Ha szükségünk van ezen node-ok listájára is, akkor használható ugyanezen függvény 1 paraméteres változata, amelynek meg kell adni, hogy rejtett(hidden) vagy kapcsolódott(connected) node-ok listájára van szükség.

C node-ok

Egy C node C nyelven írt program, ami egyfajta rejtett node az elosztott Erlangban. Erre a célra tartalmaz az Erl_Interface könyvtár függvényeket. További információk olvashatók a C node-okról a dokumentáció Erl_Interface and Interoperability Tutorial című részében.

NIF

Az Erlang filozófia szerint, igyekezzünk minden probléma megoldását abban a programozási nyelvben implementálni, amiben az a leghatékonyabb végrehajtást fogja eredményezni. Így az Erlang fejlesztői nagy hangsúlyt fektetnek a nyelv fejlesztése során arra, hogy a nyelv megőrizze nyíltságát, portolhatóságát.

Az Erlang R13B03-as verziója előtt, az Erlang node-ok nem Erlang node-okkal való összekapcsolásának a leggyakoribb módja a port driver-ek használata volt. Az Erlang R13B03-as verziójában bevezetett NIF-ek a C kódok hívásának sokkal egyszerűbb és hatékonyabb megoldását tették lehetővé. Fontosságát mutatja, hogy több régebbi alkalmazást is újraimplementáltak NIF-ek segítségével.

NIF (Native Implemented Function) egy függvény, melyet Erlang helyett hatékonysági okokból C nyelven implementáltak a fejlesztők. A NIF-ek használata az Erlang programozó szempontjából észrevehetetlen, transzparens: a NIF-es függvényeket ugyanúgy Erlang modulokba kell szervezni és ugyanúgy kell hívni, mint a natív Erlang függvényeket. A NIF-es függvények funkcionalitásának implementálása C / C++ nyelven történik, mely forrásokat felhasználva dinamikus megosztott könyvtárakat (shared library) kell linkelni belőlük (dll Windows-on, so Unix-on) ahhoz, hogy az Erlang node-unkhoz tudjuk kapcsolni őket.

Erlang-ból C kódot jelenleg a NIF segítségével lehet a lehető leggyorsabb hívni, amit többek között annak a technológiai megoldásnak köszönhetünk, hogy a NIF-es könyvtárak dinamikusan az emulátor folyamathoz vannak linkelve. A gyorsaságnak azonban ára is van: a NIF-ek használata a lehető legkevésbé biztonságos- egy NIF-es implementáción belül bekövetkező futás idejű hiba a teljes emulátort abnormális terminálásra készteti.

Hello NIF

Egy nagyon egyszerű, demó jellegű NIF-es könyvtár megírásán keresztül fogom bemutatni a technológia alkalmazásához szükséges legfontosabb tudnivalókat.

A feladatunk legyen az, hogy implementáljunk egy olyan modult, ami lehetőséget biztosít két négyzetszám differenciájának kiszámításához. A számítást implementáljuk úgy Erlang modulunkban, hogyha mindkét szám egész, akkor egy „hatékonyabb” C implementáció végezze a számítást, minden további esetben pedig Erlang modulunk. Az első esetet, mikor két egész szám a paraméter, implementáljuk úgy, hogy a tetszőleges egész számok összeszorzását, illetve összeadását C nyelven implementált NIF -es függvényekkel fogjuk elvégeztetni.

A feladat lentiekben bemutatott megoldása megtalálható és letölthető a példaprogramok között.

Erlang oldal

Egy NIF-es könyvtár használatához elengedhetetlenül szükséges egy Erlang modul implementálása is, melynek példánkban nif_demo lesz az azonosítója.

A NIF-es könyvtárakat az Erlang programozónak abban az Erlang modulban amiben használni akarja azokat explicit be kell töltenie. A betöltés elvégzésére egy „best practice” technika az, hogy kihasználjuk az Erlang-os „on_load” direktíva lehetőségét. A direktívát definiáló attribútum form-ban meg lehet adni egy a modulra nézve akár lokális, 0 aritású függvényt.

Példánkban:

-on_load(load_nif/0).

Az itt megadott függvény automatikusan meghívásra kerül, mikor a virtuális gép Code server folyamata a függvényt definiáló modult betölti. Az on_load direktívában megjelölt függvényben az erlang:load_nif/2 függvényt szükséges meghívni, átadva a betöltendő NIF-es könyvtár elérési útvonalát és a könyvtár inicializálásához szükséges értékeket paraméterül.

Példánkban:

load_nif()-> case erlang:load_nif("./priv/nif_demo", 0) of ok -> ok; _ -> unload end.

A betöltés sikerességének kimenetelétől függhet akár a teljes Erlang-os alkalmazásunk futásának eredménye, így sikertelenség esetén célszerű a NIF-et használó modult kidobatni a virtuális gép Code server-ével. Ha az on_load direktívát használva kíséreltük meg a NIF-es könyvtár betöltését, akkor sikertelenség esetén nincs más dolgunk, mint bármilyen más érvényes Erlang termmel visszatérni, mint az ok atom. Ha nem az ok atommal térünk vissza az on_load direktívában megjelölt függvényben, akkor a virtuális gép Code server-e a modul betöltését megszakítja és eltávolítja a teljes modul BEAM assembly -ét.

Ha a betöltés sikeres volt, akkor a NIF-es könyvtár függvényeinek linkelése fog következni. Mivel a NIF-ek hívása az Erlang programozó szempontjából transzparens, így a linkelés megvalósítása úgy történik, hogy a NIF-es könyvtár egy függvényét a virtuális gép a könyvtárt betöltő Erlang modulban definiált, megegyező nevű és szignatúrájú Erlang függvény definíciójához (stub) fogja linkelni. Az ilyenfajta függvénydefiníciókat NIF-es függvénycsonkoknak is szoktuk nevezni.

Példánkban az összeadó függvény csonkja (stub-ja):

add(_,_) -> exit(nif_is_not_loaded).

Ez azt vonja maga után, hogy a NIF-es könyvtár sikeres betöltése után, egy NIF-es függvény hívása esetén a vezérlés nem az Erlang-os csonkon folytatódik, hanem annak NIF-es megfelelőjén. Fontos, hogy a hívás szinkron végrehajtású, tehát a NIF-et hívó Erlang folyamat addig blokkolásra kerül, míg a NIF-es függvény vissza nem tér, visszaadva ezzel a vezérlést.

Célszerű megjegyezni, hogy bár a NIF-es függvénycsonkokat nem kötelező exportálni a modulból a helyes működéshez, de javasolt, mert előfordulhat, hogy az Erlang fordító kioptimalizálja a lokális, nem használt NIF-es függvénycsonkokat, amivel így a NIF-es könyvtár betöltése sikertelen lesz futási időben.

C/C++ oldal

Nagyon fontos észben tartani azt, hogy a NIF-ek használata során kellő körültekintéssel kell eljárnunk, ugyanis:

Míg Erlang oldalon a NIF-es könyvtárat használó Erlang modulunk alig különbözött az eddig megszokott Erlang moduloktól, addig a NIF-es függvények C oldali implementációjakor több tényezőre kell figyelnünk.

Egy modul által használt NIF-eket egy megosztott könyvtárba kell fordítani és linkelni. Egy NIF-es függvény implementációja egy C-beni függvény implementációjának feleltethető meg. Példánkban a forrásfájl neve legyen nif_demo.cpp, a linkelt állomány neve pedig nif_demo.so.

Minden NIF-es implementációt tartalmazó C forrásfájlban include-olni kell az erl_nif.h fejlécfájlt, mely többek között tartalmazza az Erlang – C, C- Erlang típusok közötti konverziókat megvalósító függvények definícióját, és további nélkülözhetetlen makrókat, szolgáltatásokat nyújt. A fejlécfájlt az Erlang gyökérkönyvtárának usr/include könyvtárában (/usr/local/lib/erlang/usr/include) találjuk meg Unix típusú rendszereken, ha az alapértelmezett paraméterekkel, forrásból fordítva telepítettük fel az Erlangot. Mivel nem végrehajtható programot fogunk fordítani a C forrásokból, így a main függvény elkészítése nem szükséges.

Az ERL_NIF_INIT makró segítségével inicializálhatjuk a NIF könyvtár használatát. Paraméterül meg kell adnunk az Erlang modulunk azonosítóját sztring határolók nélkül, és az implementált NIF függvényeket. Az implementált NIF függvényeket egy C-beni ErlNifFunc struktúrákat tartalmazó tömbként kell megadni. A struktúrával definiáljuk a NIF függvény Erlang oldali azonosítóját, aritását, és C oldali implementációját.

Példánkban:

static ErlNifFunc nif_funcs[] = { {"add", 2, add_nif}, {"mul", 2, mul_nif} }; ERL_NIF_INIT(nif_demo, nif_funcs, NULL, NULL, NULL, NULL)

A függvényparaméterek és a visszatérési értékek az ERL_NIF_TERM típus elemei. A függvényparaméterek egy argv tömbbe kerülnek, hasonlóan mint a C-s main függvényeknél már megszokhattuk. A már említett konverzió típusonként más-más függvénnyel történik, integer esetén: enif_get_int (Erlang - C) és enif_make_int (C - Erlang). Ha az enif_get_int a paraméterek alapján nem tud érvényes integer-t kiolvasni, akkor false-szal fog visszatérni. Ez esetben alkalmazhatjuk a hibakezelő függvényeket, mellyel a C oldali futást megszakítva, Erlang oldalon válthatunk ki kivételeket. Példánkban badarg kivételt fogunk kiváltani. Miután minden szükséges bemeneti paramétert C-s megfelelőjére konvertáltunk, kezdetét veheti a funkcionalitást implementáló kódrészlet. A funkcionalitás végeredményét, mint visszatérést, Erlang-os megfelelőjére kell konvertálni, majd azzal visszatérni. Nézzük meg az összeadást implementáló függvénydefiníciót:

static ERL_NIF_TERM add_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]){ // unpacking the given parameters int x,y,ret; if (!enif_get_int(env, argv[0], &x)) { return enif_make_badarg(env); } if (!enif_get_int(env, argv[1], &y)) { return enif_make_badarg(env); } // computation ret = x+y; // packing and returning the result return enif_make_int(env, ret); }
Használat

Tegyük fel, hogy az Erlang és a C kódot a nif_demo könyvtárba mentettük le.

  1. Hozzunk létre a nif_demo könyvtáron belül egy priv könyvtárat.
  2. A C kód fordítása során az alábbi parancsot kell használni, példánknál maradva az első parancs OSX-en, a második Unix típusú operációs rendszeren fogja a várt eredményt adni:
    gcc -Wall -fPIC -O3 -undefined dynamic_lookup -dynamiclib nif_demo.cpp -I /usr/local/lib/erlang/usr/include -o ./priv/nif_demo.so
    gcc -Wall -fPIC -O3 -shared nif_demo.cpp -I /usr/local/lib/erlang/usr/include -o ./priv/nif_demo.so
  3. A nif_demo könyvtárban állva indítsunk egy Erlang shell-t, az erl parancs meghívásával.
  4. Végül az Erlang shellben adjuk ki az alábbi parancsot: c(nif_demo).
  5. Ha az összes lépést végrehajtottuk úgy, hogy nem érkezett hibajelzés, az alkalmazásunk használatra kész. Az Erlang shellben, ha az alábbi parancsot adjuk ki: nif_demo:diff_of_two_squares(1,3). Akkor a NIF függvények végrehajtásával kerül kiértékelésre az eredmény, ha pedig a nif_demo:diff_of_two_squares(1.3,3.3). parancsot adjuk ki, akkor a vezérlés mindvégig Erlang oldalon marad.

Egy példa az üzembehelyezésre:

Viktoria-Fordoss-iMac:nif_demo V$ mkdir priv Viktoria-Fordoss-iMac:nif_demo V$ gcc -Wall -fPIC -O3 -undefined dynamic_lookup -dynamiclib nif_demo.cpp -I /usr/local/lib/erlang/usr/include -o ./priv/nif_demo.so Viktoria-Fordoss-iMac:nif_demo V$ erl Erlang R14B03 (erts-5.8.4) [source] [64-bit] [smp:8:8] [rq:8] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.8.4 (abort with ^G) 1> c(nif_demo). {ok,nif_demo} 2> nif_demo:diff_of_two_squares(1,3). -8 3> nif_demo:diff_of_two_squares(1.3,3.3). -9.2 4> q(). ok

Források:

Biztonság

Hitelesítés határozza meg, hogy mely node-ok tudnak kommunikálni egymással. A különböző erlang node-ok egy hálózata a lehető legalacsonyabb szinten épül be a rendszerbe, minden node rendelkezik saját magic cookie-val, ami egy erlang atom. Amikor egy node csatlakozni próbál egy másik node-hoz, akkor a magic cookie-k kerülnek összehasonlításra. Ha nem egyeznek, akkor a csatlakoztatott node elutasítja a kapcsolatot.

Induláskor egy node-hoz hozzárendelődik egy véletlen atom, mint magic cookie, és ez a cookie más node-okhoz feltételezhetően nocookie-ként rendelődik hozzá. Ezután az erlang hálózati authentikációs szerver (auth) első ízben a $HOME/.erlang.cookie nevű fáljt olvassa. Ha a fájl nem létezik, akkor létrehozza és a tartalma egy véletlen string lesz. Ha biztosak akarunk lenni benne, hogy minden lokális node-nak ugyanaz a cookie-ja, akkor beállíthatjuk manuálisan. Ez megtehető az erlang modul set_cookie(node(), Cookie) függvényével. Így a node-ok azonos cookie-val fognak rendelkekzni és szabadon tudnak kommunikálni.

Ahhoz, hogy a Node1 node a hozzárendelt Cookie cookie-val képes legyen csatlakozni, vagy kapcsolatot fogadni a Node2 node-tól egy másik DiffCookie-val, először a Node1-n meg kell hívni az erlang:set_cookie(Node2,DiffCookie) függvényt. Így lehet elosztott rendszerekben kezelni a különböző felhasználói azonosítókat kezelni.

Az az alapértelmezett, hogy ha létrejön a kapcsolat két node között, akkor azonnal az összes látható node is csatlakozik. Így mindig teljesen összefüggő hálózatot kapunk. Ha vannak node-ok különböző cookie-val, akkor ez a módszer alkalmatlan lehet, ilyenkor a -connect_all kapcsoló értékét hamisra kell állítani. A magic cookie a lokális node-on lekérdezhető az erlang:get_cookie() függvénnyel.

BIF disztribúciók

Felsorolunk néhány hasznos beépített függvényt. erlang:disconnect_node(Node): Szétkapcsolásra kényszeríti a node-ot.

erlang:get_cookie(): Visszaadja az aktuális node magic cookie-ját.

is_alive(): True-t ad vissza,ha a rendszerben egy node kapcsolódni tud más node-okhoz, false-t ad vissza egyébként.

monitor_node(Node, true|false): egy node monitor státusza, ha az értéke igazra van állítva, akkor a Node láthatóvá válik azon a node-on ahol a függvényt meghívtuk és a másik node is látni fogja a hívó csomópontot.

node(): Visszaadja az aktuális node nevét, őrfeltételek is megengedettek.

node(Arg): Visszaadja az aktuális node-ot, ahol az Arg egy folyamat azonosító vagy referencia vagy port.

nodes(): Egy listát ad vissza minden látható node-al, ami kapcsolatban áll a hívó node-al.

nodes(Arg): Az Arg paramétertől függően visszaadhatja nem csak a látható, hanem a rejtett node-okat is, ahogy azt már korábban említettük.

set_cookie(Node, Cookie): Beállítja a magic cookie-t, akkor használjuk, amikor kapcsolódni akarunk a Node-hoz.

spawn[_link|_opt](Node, Fun): Létrehoz egy folyamatot egy távoli node-on.

spawn[_link|opt] (Node, Module, FunctionName, Args): Létrehoz egy folyamatot egy távoli node-on.

Parancssori kapcsolók

-connect_all false: Csak explicit kapcsolat beállítások kerülnek felhasználásra.

-hidden: Beállítja, hogy egy node rejtett legyen.

-name: Nevet ad egy node-nak, hosszú node neveket használ.

-setcookie Cookie: Ugyanaz, mint az erlang:set_cookie(node(), Cookie).

-sname: Nevet ad egy node-nek, de a korábbi –name-el ellentétben rövid neveket használ.

node(Arg): Visszaadja az aktuális node-ot, ahol az Arg egy folyamat azonosító vagy referencia vagy port.

nodes(): Egy listát ad vissza minden látható node-al, ami kapcsolatban áll a hívó node-al.

nodes(Arg): Az Arg paramétertől függően visszaadhatja nem csak a látható, hanem a rejtett node-okat is, ahogy azt már korábban említettük.

set_cookie(Node, Cookie): Beállítja a magic cookie-t, akkor használjuk, amikor kapcsolódni akarunk a Node-hoz.

spawn[_link|_opt](Node, Fun): Létrehoz egy folyamatot egy távoli node-on.

spawn[_link|opt] (Node, Module, FunctionName, Args): Létrehoz egy folyamatot egy távoli node-on.

Modulok

Néhány hasznod modul az elosztott programozáshoz: global: Egy globális név regisztrációs szolgáltatás.

global_group:A node-ok csoportosítása globális név regisztrációs csoportok szerint.

net_adm: Különböző erlang hálózati adminisztrációs rutinok.

net_kernel: Erlang hálózati kernel.

: Létrehoz egy folyamatot egy távoli node-on.

Távoli eljárás hívási szolgáltatás (RPC)

Ha több párhuzamosan futó node-unk van, előfordulhat, hogy egy függvényt egy másik node-on akarunk futtatni, azonban ha spawn-nal indítjuk el a folyamatot, akkor a visszatérési érték elveszik. Ha szükségünk van a függvény által visszaadott értékre, akkor használjuk az RPC szolgáltatást, ami a távoli eljárás hívási szolgáltatás(remote procedure call service). Tulajdonképpen úgy működik, mint az apply függvény, meg kell neki adni, hogy melyik node-on, melyik modul, melyik függvényént, milyen argumentumokkal akarom meghívni. Majd vagy visszaadja a függvény eredményét, vagy visszatér egy {badrpc, Reason} hibajelzéssel. Van 5 paraméteres változata is, ahol megadhatunk neki még egy timeout argumentumot is. Formailag ez így néz ki:

rpc:call( Node, erlang, nodes, [ connected ] ).

Fontos megjegyezni, hogy ez a szolgáltatás, csak akkor alkalmazható, ha a másik node-n futtatni kíván függvény exportálva van. Ellenkező esetben hibát fogunk kapni.

A RPC szolgáltatásra példaprogram itt található.

El kell indítani két konzolt nevesítve, majd az egyiken meg kell hívni a node1:start(Node2) függvényt, ahol a Node2 a másik node neve. Majd fel kell tölteni a másik node-on az adatbázist a node1:upload(Node2) függvénnyel. Ezután már lehet lekérdezéseket indítani a node1:select(Node2,Arg) függvénnyel, ahol az Arg lehet user vagy price vagy admin vagy group. Ilyenkor a lekérdezés a másik node-on fut le, de az eredményt a hívó node kapja meg.