Ezzel az egész node.js "aszinkron" - "nem blokkoló" működésével kapcsolatban szerintem egy probléma van: kit érdekel? Milyen többletinformációt hordoz a programozó számára ez az egész? Ha pedig előnnyel nem jár, akkor miért így tervezték meg a nyelvet (a szemantikát)? Ha úgy vesszük, semmi sem blokkol, mert IO esetén az operációs rendszer a többi programot ugyanúgy futtatja tovább.
Azért tervezték így, mert böngésző oldali JavaScript-ben 99%-ban egyébként is így használod.
document.body.addEventListener('click', callbackFunction1);
var worker = new Worker('my_task.js');
worker.onmessage = callbackFunction2;
var xhr = new XMLHTTPRequest();
xhr.onload = callbackFunction3;
Kezdetben a Node.js 0.1 körül még a Promise-okat használtak (amiről nemrég tartottam előadást a budapest.js keretein belül), de végül áttértek a jelenlegi szintaxisra, mert ez könnyebben összeegyeztethető a böngészőbeli használattal.
De mi most nem a böngészőben, hanem a webszerveren vagyunk.
Van egy feladatom, egy helyről kérek adatokat, amit fel kell dolgoznom, addig nem tudok továbblépni. Jelent-e számomra pluszt, hogy a backend ezt szinkron vagy aszinkron oldja meg? Nem. Én a függvényemben csak aszinkron módon tudok programozni. Akkor mi az értelme? Miért kell a hardver kábelezésének kilátszania? Csak rontja az összképet.
Körülbelül tíz éve az első cégemnél a Generali biztosítónak volt egy fejlesztésünk, amiben én közvetlenül nem vettem részt, de BlaZe talán igen, ő meg tud erősíteni. Ott az volt a programozók elképzelése, hogy mivel az oldal különböző boxokból, építőtéglákból áll össze, legyen benne az URL-ben, hogy melyeknek kell az oldalon szerepelniük, tehát nagyjából így nézett ki, csak sokkal csúnyább és hosszabb volt:
akarmi.php?tegla1&tegla2=ertek&tegla3
Erre kérdeztem én, hogy mi az értelme, hisz van mondjuk tízfajta oldalunk, adjunk nekik aliast, és nem lóg ki a bele a dolognak, hívjuk őket oldal1.php-nek meg oldal2.php-nek, klasszikus szép url probléma, és analóg ezzel a blokkoló-nem blokkoló dologgal.
A Generaliban én sem vettem részt, később érkeztem. De amennyire emlékszem, ott a mögöttes elgondolás az volt, hogy több modulnak is tudjanak különböző actionöket küldeni 1 requestben.
Itt most kicsit másról van szó. Az aszinkron lényege nagyon tömören, hogy valamit futtatásra jelölsz, és azt majd valahol valaki lefuttatja, de a hívás helyén ezt neked nem kell tudni hogyan lesz meghajtva, te csak majd valamikor megkapod annak az eredményét. Ez több előnnyel is jár, de alapvetően elég jól skálázható, kontrollt ad a kezedbe a terhelés fölött.
Ha a közös munkahelyünkről keresel példát, akkor pl az exit keresője volt aszinkron megoldva.
Biztos vagyok benne, hogy maga a node.js nagyon jó alapokkal rendelkezik, és hatékonyabb programokat lehetne benne írni, mint php-ben, de szerintem amíg erre a callback-es dologra nem adnak alternatívát, addig a játékszer kategóriánál nem léphet följebb az egész.
I also realized that strong typing, which I had been worshiping for years, was somewhat of a neurotic thing. It makes you feel safe but you pay a high price for it (you write a lot of code just to please the compiler) and it actually misses the point (you should be relying on unit tests that check the semantics rather than on compilers that only check formal constraints).
Az mloc.js konferencia előadásainak a fele arról szól, miért használjunk más, erősen típusos vagy statikus típusos nyelvet, amiből aztán JS-t fordíthatunk.
Számomra ez az egész ellentmondásos. Most akkor jó vagy rossz a JS változókezelése?
Szerintem ez az egész arról szól, hogy vannak programozók, akik egy bizonyos nyelvvel eltöltöttek sok időt, aztán kapnak egy JS projektet (vagy csak elkezdenek vele spontán ismerkedni, mert most a csapból is ez folyik), ahol a megszokott dolgaikat (változó típusossága, objektumorientált programozás stb.) nem találják, és ezért mindenféle eszközöket és mintákat találnak ki, hogyan tudják JS-ben megoldani.
A magam tapasztalatából úgy látom, hogy ezekre nincs szükség. A JS meglévő eszközeivel (például prototípus alapú öröklődés, típus nélküli változók) ugyanúgy kiváló programokat lehet készíteni, mint más nyelveken, csak picit másképp kell gondolkodni közben (tehát a változókezelése jó). Ha egy másik eszközre vagyunk utalva, mondjuk egy fordítóra, ami egy bizonyos nyelvből JS-t csinál, csak a hibalehetőségek számát növeljük.
maga a node.js nagyon jó alapokkal rendelkezik, és hatékonyabb programokat lehetne benne írni, mint php-ben, de szerintem amíg erre a callback-es dologra nem adnak alternatívát
Szerintem valaki túl sokat programozott PHP-ben ;)
akik egy bizonyos nyelvvel eltöltöttek sok időt, aztán kapnak egy JS projektet (vagy csak elkezdenek vele spontán ismerkedni, mert most a csapból is ez folyik), ahol a megszokott dolgaikat (változó típusossága, objektumorientált programozás stb.) nem találják, és ezért mindenféle eszközöket és mintákat találnak ki
Mert én pont ugyanezt látom a callback-ekkel való idegenkedésben.
Az aszinkron hívásoknak ígykellene kinézniük. A kód kinézete alapján fájdalommentes megoldás, egyedül az értelmezőnek kell kicsit okosabbnak lennie.
Volt is már ilyen kezdeményezés CoffeeScript oldalon, ahol nyelvi elem lett az await és a defer (csak én speciel magával a CoffeeScript nyelvvel nem szimpatizálok).
Ahogy tudom, ilyesmi nincs sem a mostani, sem a következő ES szabványokban.
Jobban néz ki valóban, de ez is csak elfedi a mögöttes működést (ami persze fontos). És szerintem node.js-ben is egyszerű összehozni egy ilyesmit, csak függvényhívás lesz belőle, nem nyelvi elem.
Akkor valamit én félreértettem :). Nem az lenne a cél, hogy elfedjük a fejlesztő elől, hogy a háttérben egy bonyolult aszinkron hívás történik?
Őszintén szólva engem annyira a callback-piramis sem zavar, számomra átlátható (szerintem ez megszokás kérdése). Nyilván, ha a lehetőség adott, akkor valami Promise szerű dolgot használok, mert elfedi a működés egy részét, nekem kevesebbet kell kódolnom, többé-kevésbé szekvenciálisan történik a végrehajtás a kódban (jobban mondva annak látszik).
Ha lenne valami await/async nyelvi elem, akkor pedig azt használnám. Nem azért, mert valami mágikus dolgot csinálnak, hanem mert tudom, hogy nekem egyszerű használni, a háttérben pedig tudom, hogy mi történik.
A válaszod tök jogos volt :) Én azért kérdeztem, amit kérdeztem, mert nem értem, hogy HG-nak mi a baja valójában az aszinkronosdival, illetve hogy ehelyett hogy kéne működjön.
Magával az aszinkronitással nincs gondom, sőt, ha nem lenne a node-ban, akkor lennék igazán ideges. Amivel baj van, az a megvalósítása, és egyébként a blogmark is erről szól, hadd idézzem az író példáját:
For example I could write something as simple as:
total += fs.readFile(path).length;
Now, despite all the patterns that I have found, I still have to write something like:
fs.readFile(path, function(err, data) {
total += data.length;
// continue here ..
});
Nem konzekvens, ahogy az egészet kezelik. Valamilyen szinten minden függvény IO-t kezel, tehát ennyi erővel az összeshez adhattak volna callback-et:
Közben memóriát olvasunk, írunk, ami több nagyságrenddel lassabb, mint a processzor, de amíg elkészül, a CPU addig sem áll meg, hanem más szálakon dolgozhat.
Tehát még egyszer kérdem: kit érdekel, hogy egy függvény hívását szinkron vagy aszinkron módon kezeli a rendszer? Milyen információt hordoz a számomra ennek a tudása? Az én szempontomból csak az a fontos, hogy fusson le és adjon vissza valamilyen értéket, de hogy ezt hogyan teszi, az nem releváns. Felőlem azt nem blokkol, amit nem akar. Emiatt látom fölöslegesnek a callback-eket.
Tehát még egyszer kérdem: kit érdekel, hogy egy függvény hívását szinkron vagy aszinkron módon kezeli a rendszer? Milyen információt hordoz a számomra ennek a tudása? Az én szempontomból csak az a fontos, hogy fusson le és adjon vissza valamilyen értéket, de hogy ezt hogyan teszi, az nem releváns. Felőlem azt nem blokkol, amit nem akar.
A probléma ott van, hogy a te programod futása blokkolódik, ergo nem tudsz újabb kéréseket kiszolgálni (hacsak nem eleve új szálon kezded meg a kérés kezelését, ekkor pedig a szálak kezelésével kell foglalkoznod, ami szintén bonyolult probléma). Ugyanakkor ha számodra nem probléma, hogy a programod futása szünetel, akkor használhatod a függvények szinkron megfelelőit.
Az Apache HTTPD és más HTTP szerverek pont ezt a blokkolást rejtik el a felhasználó elől, mivel minden egyes kérést új szálban, vagy ahhoz hasonló kontextusban kezelnek, így önmaguk csinálnak aszinkron callback-eket.
Kérés megérkezik a HTTP szerverhez.
Indítunk egy új szálat, vagy ahhoz hasonlót, és várunk, hogy az visszatérjen (callback).
Az elindított szál visszatér az webszervernek szükséges objektummal (vagy időtúllépéssel elszáll) - ez igazából a callback.
A webszerver visszaadja megfelelő formátumban a kapott adatot.
Nem tudok elég érthetően fogalmazni, úgy látszik. Már múltkor leírtam, hogy tisztában vagyok, hogyan működik a node.js nem blokkolása. Nem azt állítom, hogy ez rossz, mert alapvetően egy jó elgondolás, tényleg szuper. Minden jó úgy aszinkron módon, ahogy van.
De.
Amikor van egy programom, abban vannak egymás után az utasítások. Ezek egy logikai egységben egymásra épülnek, vegyük ezt a leegyszerűsített példát (C-szerű szintaktikával):
1. sor: eredmeny = query('SELECT * FROM tábla'); 2. sor: for (sor in eredmeny) { 3. sor: print '<div>sor:' + eredmeny[sor] + '</div>'; 4. sor: }
A 2. sor utasítását akkor tudom végrehajtani, ha az első eredményét megkaptam, amíg nincs meg, azaz IO történik, a programom futása áll. Hogy ezt a háttérben a futtatókörnyezetem hogyan oldja meg, az nem az én dolgom. Abban viszont biztos vagyok, hogy nem állítja le a többi program futását, azaz nem blokkol. De az enyémet muszáj, nem ugorhat az 1. sor végrehajtása közben a 2.-ra. Emiatt ezt callback-ekkel megvalósítani továbbra sem látom értelmét, mert csak hosszabb lesz a kód, de nem nyerünk vele semmit.
Tehát még egyszer kérdem: kit érdekel, hogy egy függvény hívását szinkron vagy aszinkron módon kezeli a rendszer?
Egyrészt muszáj tudnod, mert az aszinkronnak pont az a lényege, hogy (valószínűleg) nem tér vissza azonnal értékkel. Ezért a válasz feldolgozását meg kell írnod valamilyen módon. Hogy ez callback, vagy más, az most tökmindegy.
Másrészt, ha nem tudod, hogy egy fv aszinkron, akkor nem is tudsz felkészülni az aszinkron működésére. Ha egy hívás result = asyncFunc() formájú lenne, akkor mitől lenne aszinkron?
Tehát elég komoly jelentősége van, hogy ezt tudd.
Illetve az IO-ban is van egy kis kavar a fejedben szerintem. Miközben a CPU a memóriát írja/olvassa, nincs üresjáratban (tekintsünk most el a hw belső működésétől). I/O művelet végzése közben pedig üresjáratban van, és ha nem tudna közben mást csinálni, akkor blockolna, vagyis kihasználatlanul hagyná az erőforrásokat értelmetlenül. Szerintem az okozza nálad a kavart, hogy hozzászoktál a multithread környezethez, ahol ilyen esetben mélyebb szinten az ütemező átadja a vezérlést egy másik szálnak. De a te szálad akkor, ott is vár. Itt ez definíció szerint kicsit máshogy van a node.js felépítése miatt.
Persze, de ahogy írtam is, nyelvi szinten nézzük a helyzetet, és nyelvi szinten a változó írása/olvasása atomi művelet, vagyis ezen a szinten a program "nem vár" (nem tud ezalatt az idő alatt mást csinálni). Hogy ez mélyebben nem teljesen így néz ki, az most kicsit más tészta, nem arról beszélünk.
Magas szintű nyelvek esetén az IO is csak függvényhívás, ami azonnal végrehajtódik. Mi persze tudjuk, hogy ez nem igaz, de ez nem változtat ezen. Éppenséggel az értékadást is végezhetnénk aszinkron módon, és a memóriára várva adhatnánk számítási feladatot a processzornak, csak nyilván nem éri meg. A memória is periféria, csak szorosan integrált.
Igen, de nézd vissza, hogy miről szól ez a topic :) A lényeg, hogy ha nagyobb adatmennyiséget szeretnél megmozgatni perifériáról, vagy lassabb perifériával dolgozol, akkor a várakozás helyett tudsz mást csinálni. Memória pedig egységnyi elérési idővel rendelkezik, azon nem tudsz spórolni, hisz mindent a memóriában tárolsz a programodhoz.
Gábor épp ezt mondta, és te ennek kapcsán írtad, hogy nem érti az IO-t. Nyilván a gyakorlatban nem lehet kihasználni a memóriaelérés alatti üresjáratot.
Van már jópár oldal ami igazolja hogy nem csak játékszer a Node.js .
Stackoverflown nagyon jó leírások vannak, hogy miért is jó link.
Asszinkronitásban az a jó ha van egy kérésnél több olyan feladat, amik időigényesek, azokat asszinkron callbackekkel tudod pl. párhuzamosítani.
Elsőre még ami tetszett benne, hogy bejövő kérésnél nem külön threadet csinál, hanem csak heap memóriát foglal neki.
Kezdem úgy érezni, hogy az aszinkron megoldás egyetlen előnye a szinkronnal szemben az alacsony memóriaigény.
Szinkron vezérlés esetén ugyanis a callback minden, ami a hívás után következik a kódban. Amennyiben egy IO-t nem igénylő számítás ideje alatt a kódnak más hasznos munkát is kellene végezni, akkor azt új szál indításával lehet megoldani. Az új szálnak azonban saját stacket kell foglalni, az ütemezését pedig a kernel végzi, a context switch pedig drága.
Aszinkron vezérlés esetén a rendszer az ütemező feladatát hárítja át a fejlesztőre, hogy ne kelljen bevonni a kernelt, ami lényegében alacsonyabb szintű kódot eredményez.
Eddig úgy tűnhet, hogy teljesítmény szempontjából az aszinkron megoldás nyer, azonban amint IO-ról van szó, a helyzet más, ugyanis a context switch ilyenkor nem kerülhető el, a hívást a kernel hajtja végre.
Ugyanannak a problémának, mégpedig a párhuzamosításnak a két különböző megközelítéséről van tehát szó, amelyben a szinkron kód mindenféleképpen magasabb absztrakciós szintet képvisel. Az egyetlen hátránya az, hogy a jelenlegi operációs rendszerek mind egyedi, statikus stacket foglalnak a szálaknak, ami magas párhuzamosság mellett magas memóriaigényhez vezet. Amennyiben erre sikerülne megoldást találni, a teljesítménybeli különbség jórészt eltűnne.
node.js esetében a feldolgozás ugyanúgy egy szálon fut, mint PHP-ban. Nem igazán látom a levezetésed alapján, mitől lenne alacsonyabb a memóriaigénye (azaz talán, de ha van is különbség, az minimális) és miért gyorsabb az aszinkron kód, kontext switch mindig kell.
node.js esetében a feldolgozás ugyanúgy egy szálon fut, mint PHP-ban
Ez így nem igaz, PHP-ban (valójában Apache-ban) kapcsolatonként egy szálon, míg a Node.js-ben szerverenként egy szálon. Ezért magasabb az előbbi memóriaigénye: minden szál rendszerszinten előre beállított méretű stackkel jön létre, függetlenül attól, szükség van-e rá. A 8 kilobyte-os Debian alapbeállítás mellett ez 10 000 élő kapcsolatnál közel 80 megabyte.
és miért gyorsabb az aszinkron kód, kontext switch mindig kell
Jogos. Amikor írtam, arra gondoltam, hogy ha csak egy számításigényes műveletet akarsz aszinkron végezni, hogy lehetőséget adj más kérések kiszolgálásának, akkor nincs szükség context switchre, de ilyenkor időzítőt kell használj, ahhoz pedig megint csak a kernelt kell hívni.
Ja, hogy a memóriaigényt úgy értetted. Igen, ez nyilvánvaló, hogy az Apache+PHP páros több memóriát eszik, viszont jobban is skálázható, mert az egyes szálakat az operációs rendszer akárhány magra ki tudja osztani. Az egyszálas futást arra írtam, hogy miután elindul egy-egy script feldolgozása, az már viszont abban a szálban marad.
Igen, ez nyilvánvaló, hogy az Apache+PHP páros több memóriát eszik, viszont jobban is skálázható, mert az egyes szálakat az operációs rendszer akárhány magra ki tudja osztani.
Pont hogy nem, és ez az igazi előnye az event-drivennek a process baseddel szemben. Event-drivennél te mondod meg, hogy hány worker fut egyszerre (és hol), process basednél pedig elindul párhuzamosan párszáz szálon a feldolgozás, aztán adott esetben nézel, hogy mi van :)
1, Nem is akarok. Azért indítottam el az adatbázislekérést, mert szükségem van az eredményre, anélkül nem tud az aktuális függvényem továbbmenni.
2, Még ha tudnék is az adatbáziskéréssel párhuzamosan csinálni bármit, az semmi komolyabb nem lehet, mivel az aktuális scriptem egy szálon fut (node.js-ben és PHP-ben is). Tehát, ha visszakerül a vezérlés a scriptemhez az adatbázistól, és még épp dolgozik valamin, akkor várni kell, amíg a munka befejeződik, hogy az adatbázistól visszakapott válasszal kezdeni tudjak valamit. Mivel nem tudom, mennyi idő az adatbázistól való kérés feldolgozása, ezért legfeljebb pár egyszerűbb utasítást adhatok ki. Amennyiben úgy jön ki, hogy az adatbázislekérés szála ugyanazon a processzoron fut, mint a scriptemé, a párhuzamosan kiadott script utasítások lassíthatják az adatbáziskérés feldolgozását.
Tehát node.js esetében igaz, futhat tovább a scriptem adatbázislekérés vagy bármely más IO mellett, de kérdéses, hogy mit nyerhetek vele, továbbá a rendszer nagyon alapos ismerete kell hozzá.
3, csak a MySQL és a PostgreSQL működését ismerem, de, feltételezem, hogy a többi is hasonló. A lekérés indítása (mysql_query()) PHP-ban valóban blokkolja a script futását úgy, hogy a válasz (hogy maga a lekérés sikeres volt-e) megérkezéséig nem tudok csinálni semmit. Viszont siker esetén az adatok beolvasása (mysql_fetch_assoc()) mindkét esetben ugyanúgy történik:
A PHP esetében a mysql_fetch_assoc() kvázi ugyanúgy nem blokkoló utasítás, mint ahogy a node működik. A node.js példát a mysql-native-pre oldalról vettem. Érzésem szerint így is meg lehetett volna írni: db.query("select 1+1,2,3,'4',length('hello')").addListener('row', function(r) { ... });
de ez a lényegen nem változtat.
1, Nem is akarok. Azért indítottam el az adatbázislekérést, mert szükségem van az eredményre, anélkül nem tud az aktuális függvényem továbbmenni.
Egy weboldalon egy oldalsó "fórum utolsó bejegyzések" boxot akarsz feltölteni tartalommal, de a requested alapvetően egy megrendelés feladásáról szól. Az utolsó hozzászólásokat simán lekérheted aszinkron, mert az eredményére csak a html kód generálásánál lesz szükséged, közben a megrendelést fel tudod dolgozni.
Nálam ez az utóbbi 7-8 évben így volt, és ez alapvető megközelítés, ha skálázható alkalmazást fejleszt az ember. Valamint te valamiért nagyon ragaszkodsz az SQL adatbázishoz, holott valószínűleg te is több tucat más forrásból is olvasol be adatot: SOAP, XMLRPC, JSON, fájlrendszer, NFS, ssh, külső process-ek stb.
Nem egyről beszélünk megint. Én event-driven webserverről beszéltem, szemben a process-baseddel, mint pl az apache. Linkek a témában pl itt és itt.
A PHP egyébként nem event-driven. Hogy a scripted alatt mi hogy mit varázsol, az teljesen más kérdés. Akkor minden event-driven, mert a kernel/cpu okos :) De a nyelv, környezet maga nem az. A PHP egy single-threaded interpreter nyelv, blokkoló perifériakezeléssel, és nem tudsz benne aszinkron hívásokat csinálni.
Caolan async-el sikerült ennyire redukálnom az eredeti kódot:
function du(path, callback) {
async.waterfall([
function info(summarize){
fs.stat(path, summarize);
},
function summarize(stat, result){
if (stat.isFile())
fs.readFile(path, function(err, data) {
if (err)
return result(err)
result(null, data.length);
});
else if (stat.isDirectory())
fs.readdir(path, function(err, files) {
if (err)
return result(err);
var total = 0;
async.forEach(files, function (file, done){
du(path + "/" + file, function(err, length) {
if (err)
return done(err);
total += length;
done();
});
}, function (err){
result(err, total);
});
});
else
result("odd file");
}
], callback);
}
Nem biztos, hogy működik, nem teszteltem. Több tapasztalattal talán még szebbet is ki lehetne hozni belőle. Én nem érzem ezt az egészet akkora problémának. Próbálnak thread-et, fiber-t, kód átfordítót, meg egy csomó mindent ráerőltetni nodejs-re emiatt, pedig szerintem teljesen szükségtelen.
Akár teljesen ki is lehet váltani waterfall-al az error check-eket, meg szétdarabolni az egészet több függvénybe:
var du = function (){
var drive = {
usage: function (path, next) {
async.waterfall([
function (next){
fs.stat(path, next);
},
function (stat, next){
if (stat.isFile())
drive.fileSize(path, next)
else if (stat.isDirectory())
drive.dirSize(path, next)
else
next("odd file");
}
], next);
},
fileSize: function (path, next){
async.waterfall([
function (next){
fs.readFile(path, next);
},
function (data, next){
next(null, data.length);
}
], next);
},
dirSize: function (path, next){
async.waterfall([
function(next){
fs.readdir(path, next);
},
function(files, next){
var total = 0;
async.forEach(files, function (file, done){
async.waterfall([
function (next){
drive.usage(path + "/" + file, next);
},
function (length, next){
total += length;
next();
}
], done);
}, function (err){
next(err, total);
});
}
], next);
}
};
return drive.usage;
}();
Picit tényleg zavaró, hogy minden aszinkron hívásnál meg kell törni az algoritmust, de ha nem jeleznénk, hogy aszinkron hívás van, akkor elég nagy bajban lennénk, ha értelmezni akarnánk a kódot...
Imho lehetne async.parallel és async.series nyelvi elemeket betenni js-be, és ezzel meg lenne oldva a probléma. Alapból minden függvény szinkron menne, ezekkel a nyelvi elemekkel meg lehetne az aszinkron kéréseket jelezni olyan helyeken, ahol tényleg szükség is van rájuk. Erre mondjuk nem egyszerű fordítót írni, ha azt nézzük, hogy minden aszinkron van megírva, és a szinkron kódot valahogyan aszinkron formára kellene hozni végső soron.
Írtam egy kis kódot, amivel sikerült tovább egyszerűsítenem. Többek között kiemeltem az err ellenőrzését a callback-ek elejéről.
Function.prototype.err = function (next){
var f = this;
return function (err){
if (err)
next(err);
else {
var args = Array.prototype.slice.call(arguments, 1);
args.push(next);
f.apply(this, args);
}
};
};
Function.prototype.waitSeries = function (){
var f = this;
for (var i=arguments.length-1; i>=0; --i)
f = arguments[i].err(f);
return f;
};
Így aszinkron:
function du (path, next) {
next.waitSeries(fs.stat, function (stat, next){
if (stat.isFile())
next.waitSeries(fs.readFile, function (data, next){
next(null, data.length);
})(null, path);
else if (stat.isDirectory())
next.waitSeries(fs.readdir, function(files, next){
var total = 0;
async.forEach(files, function (file, done){
done.waitSeries(du, function (length, next){
total += length;
next();
})(null, path + "/" + file);
}, function (err){
next(err, total);
});
})(null, path);
else
next("odd file");
})(null, path);
}
Érdemes összehasonlítani a szinkronnal:
function du (path) {
var stat = fs.stat(path);
if (stat.isFile())
return fs.readFile(path).length;
else if (stat.isDirectory()) {
var total = 0;
_.each(fs.readdir(path), function (file){
total += du(path + "/" + file);
});
return total;
}
else
throw "odd file";
}
Ha még a total feldolgozását is megtámogatnám saját függvénnyel, akkor el lehetne érni hasonló tömörséget, viszont még mindig túl sok a zaj. Azt hiszem tákolok még egy kicsit a saját lib-emen, kíváncsi vagyok meddig lehet javítani a dolgon.
Valamelyik nap én is ugyanarra a következtetésre jutottam, mint te az utolsó bekezdésben. A fordítót pedig nem is kéne módosítani, mert, mint korábban kiderült, már tudja ezt (gondolok itt arra, hogy akár a Math.abs()-t is megírhatták volna callback-kel.). Matematikailag pedig olyan kicsi az esélye, hogy valóban nyersz azon, ha párhuzamosan indítasz el folyamatokat (mindegyiket külön vas dolgozza fel), hogy inkább a normális, szinkron programozást kéne támogatni.
A node.js-t nem azért találták ki hogy kényelmesebb legyen a programozók élete, hanem főként azért, hogy egy nagyon hatékony eszközt adjon a webszerverek optimalizáláshoz. A legtöbb mai webszervernél radikális teljesítménynövekedést lehet elérni node.js-sel a hagyományos process based felálláshoz képest. Ez persze alkalmazásfüggő, nem jelent mindenre megoldást. Ha kényelmesen akarsz programozni, és nem érdekel a teljesítmény akkor tényleg nem ajánlott a node.js. Ilyenkor jobb inkább magasabb szinten programozni.
Aszinkronitás
És mi van, ha az összes szál
Azért tervezték így, mert
Böngésző?
Van egy feladatom, egy helyről kérek adatokat, amit fel kell dolgoznom, addig nem tudok továbblépni. Jelent-e számomra pluszt, hogy a backend ezt szinkron vagy aszinkron oldja meg? Nem. Én a függvényemben csak aszinkron módon tudok programozni. Akkor mi az értelme? Miért kell a hardver kábelezésének kilátszania? Csak rontja az összképet.
Körülbelül tíz éve az első cégemnél a Generali biztosítónak volt egy fejlesztésünk, amiben én közvetlenül nem vettem részt, de BlaZe talán igen, ő meg tud erősíteni. Ott az volt a programozók elképzelése, hogy mivel az oldal különböző boxokból, építőtéglákból áll össze, legyen benne az URL-ben, hogy melyeknek kell az oldalon szerepelniük, tehát nagyjából így nézett ki, csak sokkal csúnyább és hosszabb volt:
akarmi.php?tegla1&tegla2=ertek&tegla3
Erre kérdeztem én, hogy mi az értelme, hisz van mondjuk tízfajta oldalunk, adjunk nekik aliast, és nem lóg ki a bele a dolognak, hívjuk őket oldal1.php-nek meg oldal2.php-nek, klasszikus szép url probléma, és analóg ezzel a blokkoló-nem blokkoló dologgal.
A Generaliban én sem vettem
Itt most kicsit másról van szó. Az aszinkron lényege nagyon tömören, hogy valamit futtatásra jelölsz, és azt majd valahol valaki lefuttatja, de a hívás helyén ezt neked nem kell tudni hogyan lesz meghajtva, te csak majd valamikor megkapod annak az eredményét. Ez több előnnyel is jár, de alapvetően elég jól skálázható, kontrollt ad a kezedbe a terhelés fölött.
Ha a közös munkahelyünkről keresel példát, akkor pl az exit keresője volt aszinkron megoldva.
Én a függvényemben csak
utasítás 2
utasítás 3
Ezek egymásra épülnek.
Folytatás
Számomra ez az egész ellentmondásos. Most akkor jó vagy rossz a JS változókezelése?
Szerintem ez az egész arról szól, hogy vannak programozók, akik egy bizonyos nyelvvel eltöltöttek sok időt, aztán kapnak egy JS projektet (vagy csak elkezdenek vele spontán ismerkedni, mert most a csapból is ez folyik), ahol a megszokott dolgaikat (változó típusossága, objektumorientált programozás stb.) nem találják, és ezért mindenféle eszközöket és mintákat találnak ki, hogyan tudják JS-ben megoldani.
A magam tapasztalatából úgy látom, hogy ezekre nincs szükség. A JS meglévő eszközeivel (például prototípus alapú öröklődés, típus nélküli változók) ugyanúgy kiváló programokat lehet készíteni, mint más nyelveken, csak picit másképp kell gondolkodni közben (tehát a változókezelése jó). Ha egy másik eszközre vagyunk utalva, mondjuk egy fordítóra, ami egy bizonyos nyelvből JS-t csinál, csak a hibalehetőségek számát növeljük.
maga a node.js nagyon jó
Szerintem valaki túl sokat programozott PHP-ben ;)
Mert én pont ugyanezt látom a callback-ekkel való idegenkedésben.
Mi a bajod igazából az
Az aszinkron hívásoknak így
Volt is már ilyen kezdeményezés CoffeeScript oldalon, ahol nyelvi elem lett az
await
és adefer
(csak én speciel magával a CoffeeScript nyelvvel nem szimpatizálok).Ahogy tudom, ilyesmi nincs sem a mostani, sem a következő ES szabványokban.
Jobban néz ki valóban, de ez
Akkor valamit én
Őszintén szólva engem annyira a callback-piramis sem zavar, számomra átlátható (szerintem ez megszokás kérdése). Nyilván, ha a lehetőség adott, akkor valami Promise szerű dolgot használok, mert elfedi a működés egy részét, nekem kevesebbet kell kódolnom, többé-kevésbé szekvenciálisan történik a végrehajtás a kódban (jobban mondva annak látszik).
Ha lenne valami await/async nyelvi elem, akkor pedig azt használnám. Nem azért, mert valami mágikus dolgot csinálnak, hanem mert tudom, hogy nekem egyszerű használni, a háttérben pedig tudom, hogy mi történik.
A válaszod tök jogos volt :)
Magával az aszinkronitással
total += fs.readFile(path).length;
Now, despite all the patterns that I have found, I still have to write something like:
fs.readFile(path, function(err, data) {
total += data.length;
// continue here ..
});
valtozo = data;
});
Közben memóriát olvasunk, írunk, ami több nagyságrenddel lassabb, mint a processzor, de amíg elkészül, a CPU addig sem áll meg, hanem más szálakon dolgozhat.
Tehát még egyszer kérdem: kit érdekel, hogy egy függvény hívását szinkron vagy aszinkron módon kezeli a rendszer? Milyen információt hordoz a számomra ennek a tudása? Az én szempontomból csak az a fontos, hogy fusson le és adjon vissza valamilyen értéket, de hogy ezt hogyan teszi, az nem releváns. Felőlem azt nem blokkol, amit nem akar. Emiatt látom fölöslegesnek a callback-eket.
Te programod
A probléma ott van, hogy a te programod futása blokkolódik, ergo nem tudsz újabb kéréseket kiszolgálni (hacsak nem eleve új szálon kezded meg a kérés kezelését, ekkor pedig a szálak kezelésével kell foglalkoznod, ami szintén bonyolult probléma). Ugyanakkor ha számodra nem probléma, hogy a programod futása szünetel, akkor használhatod a függvények szinkron megfelelőit.
Az Apache HTTPD és más HTTP szerverek pont ezt a blokkolást rejtik el a felhasználó elől, mivel minden egyes kérést új szálban, vagy ahhoz hasonló kontextusban kezelnek, így önmaguk csinálnak aszinkron callback-eket.
Működés
De.
Amikor van egy programom, abban vannak egymás után az utasítások. Ezek egy logikai egységben egymásra épülnek, vegyük ezt a leegyszerűsített példát (C-szerű szintaktikával):
2. sor: for (sor in eredmeny) {
3. sor: print '<div>sor:' + eredmeny[sor] + '</div>';
4. sor: }
A 2. sor utasítását akkor tudom végrehajtani, ha az első eredményét megkaptam, amíg nincs meg, azaz IO történik, a programom futása áll. Hogy ezt a háttérben a futtatókörnyezetem hogyan oldja meg, az nem az én dolgom. Abban viszont biztos vagyok, hogy nem állítja le a többi program futását, azaz nem blokkol. De az enyémet muszáj, nem ugorhat az 1. sor végrehajtása közben a 2.-ra. Emiatt ezt callback-ekkel megvalósítani továbbra sem látom értelmét, mert csak hosszabb lesz a kód, de nem nyerünk vele semmit.
Tehát még egyszer kérdem: kit
Egyrészt muszáj tudnod, mert az aszinkronnak pont az a lényege, hogy (valószínűleg) nem tér vissza azonnal értékkel. Ezért a válasz feldolgozását meg kell írnod valamilyen módon. Hogy ez callback, vagy más, az most tökmindegy.
Másrészt, ha nem tudod, hogy egy fv aszinkron, akkor nem is tudsz felkészülni az aszinkron működésére. Ha egy hívás result = asyncFunc() formájú lenne, akkor mitől lenne aszinkron?
Tehát elég komoly jelentősége van, hogy ezt tudd.
Illetve az IO-ban is van egy kis kavar a fejedben szerintem. Miközben a CPU a memóriát írja/olvassa, nincs üresjáratban (tekintsünk most el a hw belső működésétől). I/O művelet végzése közben pedig üresjáratban van, és ha nem tudna közben mást csinálni, akkor blockolna, vagyis kihasználatlanul hagyná az erőforrásokat értelmetlenül. Szerintem az okozza nálad a kavart, hogy hozzászoktál a multithread környezethez, ahol ilyen esetben mélyebb szinten az ütemező átadja a vezérlést egy másik szálnak. De a te szálad akkor, ott is vár. Itt ez definíció szerint kicsit máshogy van a node.js felépítése miatt.
Illetve az IO-ban is
Dehogynem, wait state-nek hívják.
Persze, de ahogy írtam is,
Tisztán nyelvi szempontból
Nem is mondtam, hogy más.
Magas szintű nyelvek esetén
Igen, de nézd vissza, hogy
Gábor épp ezt mondta, és te
Játékszer???
Stackoverflown nagyon jó leírások vannak, hogy miért is jó link.
Asszinkronitásban az a jó ha van egy kérésnél több olyan feladat, amik időigényesek, azokat asszinkron callbackekkel tudod pl. párhuzamosítani.
Elsőre még ami tetszett benne, hogy bejövő kérésnél nem külön threadet csinál, hanem csak heap memóriát foglal neki.
Kezdem úgy érezni, hogy az
Szinkron vezérlés esetén ugyanis a callback minden, ami a hívás után következik a kódban. Amennyiben egy IO-t nem igénylő számítás ideje alatt a kódnak más hasznos munkát is kellene végezni, akkor azt új szál indításával lehet megoldani. Az új szálnak azonban saját stacket kell foglalni, az ütemezését pedig a kernel végzi, a context switch pedig drága.
Aszinkron vezérlés esetén a rendszer az ütemező feladatát hárítja át a fejlesztőre, hogy ne kelljen bevonni a kernelt, ami lényegében alacsonyabb szintű kódot eredményez.
Eddig úgy tűnhet, hogy teljesítmény szempontjából az aszinkron megoldás nyer, azonban amint IO-ról van szó, a helyzet más, ugyanis a context switch ilyenkor nem kerülhető el, a hívást a kernel hajtja végre.
Ugyanannak a problémának, mégpedig a párhuzamosításnak a két különböző megközelítéséről van tehát szó, amelyben a szinkron kód mindenféleképpen magasabb absztrakciós szintet képvisel. Az egyetlen hátránya az, hogy a jelenlegi operációs rendszerek mind egyedi, statikus stacket foglalnak a szálaknak, ami magas párhuzamosság mellett magas memóriaigényhez vezet. Amennyiben erre sikerülne megoldást találni, a teljesítménybeli különbség jórészt eltűnne.
Javítson ki, aki másképp látja.
Memóriaigény
node.js esetében a
Ez így nem igaz, PHP-ban (valójában Apache-ban) kapcsolatonként egy szálon, míg a Node.js-ben szerverenként egy szálon. Ezért magasabb az előbbi memóriaigénye: minden szál rendszerszinten előre beállított méretű stackkel jön létre, függetlenül attól, szükség van-e rá. A 8 kilobyte-os Debian alapbeállítás mellett ez 10 000 élő kapcsolatnál közel 80 megabyte.
Jogos. Amikor írtam, arra gondoltam, hogy ha csak egy számításigényes műveletet akarsz aszinkron végezni, hogy lehetőséget adj más kérések kiszolgálásának, akkor nincs szükség context switchre, de ilyenkor időzítőt kell használj, ahhoz pedig megint csak a kernelt kell hívni.
Memória
"Igen, ez nyilvánvaló, hogy
Pont hogy nem, és ez az igazi előnye az event-drivennek a process baseddel szemben. Event-drivennél te mondod meg, hogy hány worker fut egyszerre (és hol), process basednél pedig elindul párhuzamosan párszáz szálon a feldolgozás, aztán adott esetben nézel, hogy mi van :)
A PHP is event driven, csak
Csak addig nem nagyon tudsz
1, Nem is akarok. Azért
2, Még ha tudnék is az adatbáziskéréssel párhuzamosan csinálni bármit, az semmi komolyabb nem lehet, mivel az aktuális scriptem egy szálon fut (node.js-ben és PHP-ben is). Tehát, ha visszakerül a vezérlés a scriptemhez az adatbázistól, és még épp dolgozik valamin, akkor várni kell, amíg a munka befejeződik, hogy az adatbázistól visszakapott válasszal kezdeni tudjak valamit. Mivel nem tudom, mennyi idő az adatbázistól való kérés feldolgozása, ezért legfeljebb pár egyszerűbb utasítást adhatok ki. Amennyiben úgy jön ki, hogy az adatbázislekérés szála ugyanazon a processzoron fut, mint a scriptemé, a párhuzamosan kiadott script utasítások lassíthatják az adatbáziskérés feldolgozását.
Tehát node.js esetében igaz, futhat tovább a scriptem adatbázislekérés vagy bármely más IO mellett, de kérdéses, hogy mit nyerhetek vele, továbbá a rendszer nagyon alapos ismerete kell hozzá.
3, csak a MySQL és a PostgreSQL működését ismerem, de, feltételezem, hogy a többi is hasonló. A lekérés indítása (
mysql_query()
) PHP-ban valóban blokkolja a script futását úgy, hogy a válasz (hogy maga a lekérés sikeres volt-e) megérkezéséig nem tudok csinálni semmit. Viszont siker esetén az adatok beolvasása (mysql_fetch_assoc()
) mindkét esetben ugyanúgy történik:node.js:
cmd.addListener('row', function(r) {
parancs1(r);
parancs2(r);
parancs3(r);
});
}
var cmd = db.query("select 1+1,2,3,'4',length('hello')");
dump_rows(cmd);
PHP:
while ($r = mysql_fetch_assoc($cmd)) {
parancs1($r);
parancs2($r);
parancs3($r);
}
A PHP esetében a
mysql_fetch_assoc()
kvázi ugyanúgy nem blokkoló utasítás, mint ahogy a node működik. A node.js példát a mysql-native-pre oldalról vettem. Érzésem szerint így is meg lehetett volna írni:db.query("select 1+1,2,3,'4',length('hello')").addListener('row', function(r) { ... });
de ez a lényegen nem változtat.
1, Nem is akarok. Azért
Egy weboldalon egy oldalsó "fórum utolsó bejegyzések" boxot akarsz feltölteni tartalommal, de a requested alapvetően egy megrendelés feladásáról szól. Az utolsó hozzászólásokat simán lekérheted aszinkron, mert az eredményére csak a html kód generálásánál lesz szükséged, közben a megrendelést fel tudod dolgozni.
Ez így igaz, ha tudod
Nálam ez az utóbbi 7-8 évben
Szerencsésnek mondhatom
Tipikusan Node-ban sem tudsz
Nem egyről beszélünk megint.
A PHP egyébként nem event-driven. Hogy a scripted alatt mi hogy mit varázsol, az teljesen más kérdés. Akkor minden event-driven, mert a kernel/cpu okos :) De a nyelv, környezet maga nem az. A PHP egy single-threaded interpreter nyelv, blokkoló perifériakezeléssel, és nem tudsz benne aszinkron hívásokat csinálni.
Van különbség a process based
Caolan async-el sikerült
Akár teljesen ki is lehet váltani waterfall-al az error check-eket, meg szétdarabolni az egészet több függvénybe:
Imho lehetne async.parallel és async.series nyelvi elemeket betenni js-be, és ezzel meg lenne oldva a probléma. Alapból minden függvény szinkron menne, ezekkel a nyelvi elemekkel meg lehetne az aszinkron kéréseket jelezni olyan helyeken, ahol tényleg szükség is van rájuk. Erre mondjuk nem egyszerű fordítót írni, ha azt nézzük, hogy minden aszinkron van megírva, és a szinkron kódot valahogyan aszinkron formára kellene hozni végső soron.
Írtam egy kis kódot, amivel sikerült tovább egyszerűsítenem. Többek között kiemeltem az err ellenőrzését a callback-ek elejéről.
Szinkron
Hát szerintem is jobb lenne,
A node.js-t nem azért