PHP --> NodeJS - a legelso lepes mielott atternel

Mindenkinek elfelejtik elmondani az elso lepest, aki PHP-bol vagy egyeb proceduralisan mukodo nyelvbol a NodeJS vilagaba terne at. Egyvalamiben ugyanis maskepp kell gondolkodni.

Pelda: proceduralis pszeudokod (PHP-ban, de meg C/C++-ban es Java-ban is ezt szoktad meg):


Utasitas1();
Utasitas2();
Utasitas3();
Utasitas4();
Utasitas5();
Utasitas6();
Utasitas7();
Utasitas8();
Utasitas9();
Utasitas10();

Nos, tegyuk fel hogy Utasitas 1 utan futhat le csak a 2-es utana a 3-as. A 3 utan lefuthat a 4, es az 5, de a 4 nem elokovetelmenye az 5-nek (pl. az egyik csak egy tracking, hogy a 3-as sikerult). A 6 -> 7 -> 8 -> 9 igazabol mar a futasido kezdeten lefuthatna ebben a sorrendben, nem fugg az 1-es utasitastol sem. A 10-eshez le kell futnia a 4-esnek es a 9-esnek.

Ennek ellenere PHP-ban ezek biztos hogy csak egymas utan fognak lefutni. a multiprocessing ott azt jelenti, hogy 250 ilyen keres mehet parhuzamosan, de ez mindegyiknel sorrendben lesz, nem is szamit.

NodeJS-ben egesz maskepp kell gondolkodnod. Meg maguk a db libek is async hivast csinalnak, ergo:


save1();
save2();
save3();

Ha fugg egymastol az amugy async save1() save2() es save3(), akkor vagy callback-be kell tenni (sikerult vagy nem sikerult fuggveny), vagy fire-olni kell egy eventet es annak a listenerebe kell tenni, vagy promise-ba kell tenni es annak a .then() fuggvenyebe


var saveret1 = save1();
saveret1.onDone(function{
  var saveret2= save2();
  ...
});

Nem feltetlen kell ezeket undoritoan egymasba agyazni. Lehet globalis event listener ami done-kor fire-olodik mint event, vannak async dependency kezelo libek, amik segitenek egymastol fuggetlenul lefuttatni ezeket a megfelelo fuggosegi sorrendekben, de sajnos meg db muveletekhez is egyszeruen EL KELL FELEJTENED a proceduralis egymas utan irast nemtrivialis muveletekre (pl db lekerdezes). Cserebe jobban multiprocess ready lesz a kodod akaratlanul is.

Tehat roviden osszefoglalva: itt nem egymas utan lefuto dolgokat definialsz. Itt sorrendeket es process-ek kozti fuggosegi tablakat definialsz.

Magyarazo pszedudokod (nem valid nodejs kod) a fenti PHP kod NodeJS-es valtozatara:


var can10run = 0; // trivialis muvelet, szinkron lefut, kovetkezo sor elott
var can10runlistener = new eventListener(); // trivialis muvelet, szinkron lefut, kovetkezo sor elott

Utasitas1().done(Utasitas2().done(Utasitas3().done( function { // mind aszinkron, done-ra hivjak meg egymast
  Utasitas4().done(function {
    can10run++;
    can10runlistener.triggerEvent();
  });
  Utasitas5(); // Ennek a done-ja nem is erdekel minket, mindegy lefutott-e, csak elindul ugy async
})));

// az Utasitas1()-ben bealllitott valtozot itt jo esellyel nem latod

Utasitas6().done(Utasistas7.done(Utasitas8.done(Utasitas9.done( function () { // az Utasitas 1-gyel egyidoben elkezdodik
  can10run++;
  can10runlistener.triggerEvent();
}))));

onEvent(can10runlistener) {
  if (can10run == 2) { // akkor fut le, ha Utasitas4() es Utasitas9() is vegzett, mert azok az elofelteteleik, mindegy milyen sorrendben tortent ez.
    Utasitas10(); // mint minden mas, ez is fut a hatterben
  }
}

Olvashatatlanabb igen. De cserebe Utasitas1() es Utasitas6() egyszerre lefut mindjart az elejen pl., igy kihasznalva, hogy a gepedben bizony nem csak egymagos processzor van (fixed in comments). Vannak szebb, de kevesbe kezdobarat megvalositasok ezekre, de jobb ha megszokja a szemed ezt a stilust, mert ennel sokkal olvashatobb sose lesz, tanuld meg az event listenereket es az eventek sorrendjet ertelmezni.

Es a legfontosabb: kerlek a nagy tomeggel ellentetben legyszives csinalj olyan kodot, ahol nagyjabol azert egy helyen vannak azok az eventek, amik egymastol fuggnek. Ugyanis ebben a kornyezetben nagyon konnyu write-only kodot irni, ahol osszevissza fut 30 forrasfajl random fuggvenye es event listenere.

Sok helyen lattam mar NodeJS tutorialokat, de ezt a leges legelso lepest atteres elott hianyoltam. Remelem ezzel masoknak segithettem, mert nekem rengeteg idobe tellett felfognom.

Hozzászólások

"De cserebe Utasitas1() es Utasitas6() egyszerre lefut mindjart az elejen pl., igy kihasznalva, hogy a gepedben bizony nem csak egymagos processzor van."

Ez sajnos nem igaz, a javascript futtato kornyezet szigoruan 1 magon (es egy szalban) fut, az IO muveletek (net/disk/db) tudnak csak ebbol profitalni.

Így van, ezt lenne a legfontosabb megérteni. Egyszálúság van. Ez a kódban bár hátránynak tűnik (sok php-n/java-n szocializálódott fejlesztő nem képes ezt a gátat leküzdeni), ugyanakkor pont ennek is köszönheti a jó skálázódási tulajdonságait. Mind development, mind futtatás szempontjából.

> Így van, ezt lenne a legfontosabb megérteni. Egyszálúság van. Ez a kódban bár hátránynak tűnik (sok php-n/java-n szocializálódott fejlesztő nem képes ezt a gátat leküzdeni), ugyanakkor pont ennek is köszönheti a jó skálázódási tulajdonságait. Mind development, mind futtatás szempontjából.

Ezt elmagyaráznád nekem, miért?
--
blogom

A developernek jó, mert nem küzd lockokkal, happens-before-ral és a többi nagyon bonyolult™ koncepttel.
A processzormagnak jó, mert nagyságrendekkel kevesebb context switch van.

A Moore úr "törvényéből" levezetett vertikális performancianövekedés megállt, hát jönnek az olyan eszközök, amik a horizontális felé terelik a népet. Ilyen a node.js-ökoszisztéma. Ez sem a szent grál, csak itt más konceptekkel kell bajlódni (clustering, async - amiről a kezdő poszt is szól, stb.)

A javascript, event based modelljében léptékekkel olcsóbb egy "szál" létrehozása, mint ha külön thread-et vagy process-t használna minden lekérdezés kiszolgálására. Persze ennek is meg van a határa, ezért szoktak rendes webszervert node elé rakni, ami több process-ben futtatja a node-os lekérdezésket.

Szerintem elbeszélünk egymás mellett. Én arról beszélek, hogy ha van két párhuzamosan végezhető feladat, akkor miért jó az, hogy gyakorlatilag sorosítjuk? Mert egy szálon gyakorlatilag sorosítva tud csak futni két dolog.

----------------
Lvl86 Troll, "hobbifejlesztő" - Think Wishfully™

Utananezve azzal ervelnek nodejs-ek, hogy ne kelljen egy valtozot tobb thread/process kozt megosztani, mert az tul bonyolult konkurens eseteket eredmenyezne, amit jobb ha a fejlesztonek nem kell ismernie. Helyette lehet csinalni worker process-eket (ugy vettem ki hogy azt sem annyira regota), de azok nem ismerhetik egymas valtozoit. Nincs olyan szofisztikalt megoldas, mint pl a volatile kulcsszo C++-ban es Javaban.

A Node.JS-t tipikusan arra használják, hogy HTTP requestként bejön egy kérés, azzal valamit kezdeni kell, pl. adatbázisból előkotorni valamit, és visszaadni szépen formázott HTML-ként vagy JSON-ként.

A Node.JS azt mondja, hogy nem kell kliensenként egy szálat indítani, csak azért, hogy az várakozzon. Helyette callback-es aszinkron megoldást implementáltak, ami jól illeszkedik a JavaScript logikájához.

Egyébként eredetileg nem is JS-t választott a szerző, hanem Luát. Kár, hogy ez elmúlt...

Fuszenecker Róbert

Értem. Ez mondjuk számomra ebben a formában nem felskálázódás, csupán egy értelmesebb implementáció a Single thread feldolgozásra.

Jó oké persze, egy CPU magra nézve igen, de ma már lassan a mobilokban is 8 core van...

----------------
Lvl86 Troll, "hobbifejlesztő" - Think Wishfully™

Persze, hogy lehetnek. De pont ez a lényeg: párhuzamosan végezhető, egymástól elszeparált feladatok igazából olyanok, hogy n db egyszálú alkalmazásod fut egymás mellett. Semmi konkurencia nincs benne.

NodeJS alatt is futhat több processzed (több JS threaded fut, egymástól teljesen függetlenül).

Hogy értsd:
Klasszikus konkurens modelben, ahol hiába van több szál, a fő feldolgozandó, szálak között megosztott adatstruktúrát (legyen ez a HTTP request/response pár) úgyis sorosítva éred el, ha nem akarsz konzisztenciahibát.
Namármost, a JS programozási modelljében nem lockokat használsz, hogy a request/response-on végzett műveleteket sorosítsad (és a műveletek hívási sorrendje teteszúleges, legfeljebb várnak a threadek a lockokra - azaz implicit a hívási lánc), hanem explicit hívási lánc van.

és akkor a fenti példa* miért jobb a JS-es, callbackes formában, mint így, egymás után írva?
Hiszen pont az veszik el belőle, hogy a

feladat3()

majd párhuzamosan (másik feldolgozón?) fut amíg a itt folytatódik a végrehajtás, nem? Mert az egész __egy__ szálon hajtódik végre, csak nem az általam megadott sorrendben, hanem ahogy az interpreter jónak látja (ez előny, hogy az interpreter dönt, elismerem. de továbbra is egy szál)

ehhez képest Java-ban (s valszeg .NET-ben is), az egész feladat1-3-5-t kirakom egy AsyncTask-ra, s az onnantól külön szál. Ugyanez a paralellStreamekkel.

Még mindig nem értem, miért jobb az előbbi, pedig tényleg érdekel.

*:


feladat1();
feladat2();
feladat3(); // függ az 1-től
feladat4();
feladat5(); // függ a 3-tól
feladat6(); // függ a 4-től

--
blogom

Szerintem azt a részét nem érted a dolgoknak, hogy itt a taszkokat mindenképp sorosítani kell, mert ugyanahhoz az adatstruktúrához férnek hozzá (ugyanaz a global scope).
Hogy értsd: feladat3() függ 1-től (nem futhat 1 előtt), de nem futhat feladat2()-vel párhuzamosan sem.

Ez az egész egy happens-before gráfot ír le. Nem párhuzamosításról, hanem sorosításról van szó.
Javaban ugyanezt lockokkal oldod meg, lockokkal írod le a happens-before relációkat.

Illetve azt fejezed még ki így, hogy feladat2() elindulhat anélkül, hogy feladat1() ténylegesen véget érne (mondjuk inputra vár, vagy DB írást csinál).

Ugyebár az, ha több szálad van, de valójában a lockolások miatt mindig csak 1 fut, akkor a kódod semmit nem nyert a (formai) többszálúságon :)

Szerintem te ezt kevered a ServiceWorkerrel, az a konkurens programozás (azaz amikor nincs adatfüggés két végrehajtható kód között) JS-es megoldása.

Hogy a JS-hez hasonlítsam: ez a feladat1()-feladatn() kód az AsyncTaskok vezérlő kódja: minden egyes feladatN() az egy AsyncTask-Future indítás lehet.

Meg azért ugye azt vegyük észre, hogy ha 1 CPU-n futsz, akkor teljesen mindegy, hány szálad vna, egyszerre csak egy műveletet végez ez a gép :)

A feladat1().then(feladat3).then(feladat4) ugyebár annak felel meg, amikor azt mondod, hogy (pszeudokód)

future1 = feladat1.execute();
future1.get(); // ez megvárja feladat1-et
future3 = feladat3.execute();
etc.

Az, hogy te eközben még leírod a feladat1().execute() után, hogy feladat2().execute, azzal ugyanott vagy, mint a JS-es kódban:
a feladat2-t végrehajtod, MAJD a feladat3 csak akkor indul, ha feladat1 és feladat2 is végzett.
Azaz feladat3 nem fut most sem semmivel párhuzamosan, hiába formailag külön szál.
Azt értem, hogy JS-ben feladat1() és feladat2() egy szálon fut, mert így indítjuk el, és lehetne párhuzamosítani, de erre más technika való.
A JS-es szintakszist képzeld úgy, hogy 1 szálban futtatsz sok függvényt, és mindegyik Future-rel tér vissza, nem kell külön lockolnod a dolgokat, hanem a hívások struktúrája (meg a Future-ok bevárása) leírja azt, hogy pontosan mi mi után fog történni.

Javaban meg ugye figyelned kell arra, hogy ha több szálon indítod el a dolgokat, akkor kikényszerítsd a happens-before relációt.

Igazából az egész csak syntax.
Azt kell megérteniük az embereknek, hogy az I/O nem azonnali művelet, és amíg az I/O processzor vár (lehet ez egy USB-s billentyűzet, meg egy Ethernet chip is), addig is végezhet a CPU hasznos munkát. Ez az a gondolkodásmód, amit meg kell érteni. Az, hogy ezt hogyan fejezed ki szintakszisban, megint más kérdés.

mondjuk 5 kulonbozo VM-ben fut?

Ha minden non-blocking (callback, promise, event), akkor teljesen vallalhato.

Egyebkent a node.js-esek most akarnak worker thread-et letrehozni.

---
Saying a programming language is good because it works on all platforms is like saying anal sex is good because it works on all genders....

Az, hogy egy szal van, egyatalan nem teszi konnyebbe a skalazhatosagot. Konnyebb igy fejleszteni, es egymas melle tenni szimplabb szeleteket. De a skalazodas nem olyan egyszeru, hogy elinditasz N darabot belole, max ha teljesen stateless, vagy semmilyen modon nem kell kommunikalniuk. Ha kell, akkor kb ugyanott tartasz, csak egyszerubb komponenseid vannak, cserebe nehezebb - es koltsegesebb - lesz a kommunikacio.

--
|8]

Promise-ek, generátorok, stb. - olvass utána.

És igen, kulturált fejlesztő "az eventeket", vagyis az IO-t különválasztja a nem
IO-tól, tesztelés, concurrency, stb. miatt. Ez nem Node, nem JS specifikus, de
illene így lennie bármilyen nyelvről is beszélünk.

"olvass utána"

Azoknak keszult ez, akik meg csak fogjak ezeket olvasni, vagy mar akar felfogas nelkul olvastak is, de tippjuk sincs miert van rajuk egyaltalan szukseg. Pont az "olvass utana"-kat elkerulendo, ertsek mit olvasnak es mit hol keressenek es miert. Vagy valahol netan teves informaciot kozoltem volna a promise-okrol? Mert azt akkor javitom (meg en se vagyok ezzel nagyon kepben). A celja ennek az volt, hogy tudd, hova nezelodj tovabb, megse legyen semmi tul hosszan magyarazva.

"Olvashatatlanabb igen. De cserebe Utasitas1() es Utasitas6() egyszerre lefut mindjart az elejen pl., igy kihasznalva, hogy a gepedben bizony nem csak egymagos processzor van (fixed in comments). Vannak szebb, de kevesbe kezdobarat megvalositasok ezekre, de jobb ha megszokja a szemed ezt a stilust, mert ennel sokkal olvashatobb sose lesz, tanuld meg az event listenereket es az eventek sorrendjet ertelmezni."

Ez nem igaz, igenis lehet olvasható async kódot írni, csak meg kell ismerni az eszközöket.

Én, mint már immár lassan 3 hónapja tisztán frontend fejlesztő lévén tanúsíthatom mekkora szopacs és kíllódás átállni erre a gondolkozásra.
De nem lehetetlen. :)

---------------------------------------
Devmeme - fejlesztői pillanatok

Ez ugyanaz a csodaság, ami legutóbb a leftpad-vs-kik üggyel hívta fel magára a figyelmet? Mert az szemre teljesen normális szekvenciális kódnak látszott. (Mind a 11 sor.)

Az proceduralis fuggveny volt, nem volt benne async hivas. Amit most leirtam (elsore rosszul, de javitva) az az async mukodese, kommentekben kozben kiegeszult hogy egy CPU varakozasi sorrendjet lovod be ilyenkor. De ez a leftpadot nem erintette, abban semmi async nem volt, az egy kivetelesen szinkron JS kod volt. Azok megvarjak az elozo (trivialis) utasitast.