ugrás a tartalomhoz

Web Worker – Számolni? Böngészőben?!

presidento · 2010. Ápr. 26. (H), 09.39
Web Worker – Számolni? Böngészőben?!

Kezdetben teremtetett vala a karakteres konzol és a HTML dokumentum. De a konzol sötét (hátterű) vala, ezért lőn világosság és grafikus felület. A HTML dokumentum azonban kietlen és puszta maradt. Hogy a dokumentumnak szépséget adjon, teremté a Programozó a CSS-t, s hogy funkcionalitást és interaktivitást adjon neki, teremté a JavaScriptet. És látá a Programozó, hogy ez jó.

Idővel felnövekedék a Programozó Fia, és feltette a kérdést: „Apám! A JavaScript egy programozási nyelv?” „Igen”, szólt a válasz. „De Apám! Akkor miért nem írunk programot JavaScriptben?” „Fiam – felelt az Apa –, a JavaScriptet nem erre találták ki.” A Fiút azonban nem hagyta nyugodni a gondolat. Miért tart itt a világ?

A jelenlegi helyzet két okra vezethető vissza: egyrészt a JavaScript egy köztudottan lassú nyelv. És ez a tapasztalat annak ellenére, hogy a böngészők gyártói versenybe hajszolták egymást, egyre gyorsabban teljesítik a kifejezetten JavaScript számára írt benchmarkokat, még az Internet Explorer 9 is azzal dicsekszik, hogy gyorsabb lesz, mint a Firefox. No, de mit mutatnak a száraz tények? Keressük meg a 35. fibonacci számot:

function fibonacci(n) {
    return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}

var start = new Date();
var fib   = fibonacci(35);
var end   = new Date();

alert((end - start) / 1000.0 + ' (' + fib + ')');

A fentivel lényegében egyező a kódot a különböző nyelveken futtatva a következő eredményt kaptam (futásidő másodpercben):

A mérés eredményei diagramon

Nem várt eredmény: azonos gépen futtatva a szerver oldali nyelvek közül csak a C, a Java és a JavaScript (Node.js) képes felvenni a versenyt a modern böngészők JavaScript sebességével. (Valószínűleg a .NET is, ezt nem volt lehetőségem tesztelni. Az IE9 előzetes az IE8 és a Firefox között van, a használt gépen nem állt rendelkezésemre.) Igaz, ez a teszt erősen épít a függvényhívásokra. Számítási műveleteket végezve nincsenek ekkora különbségek, de a legelterjedtebb szerver oldali nyelvnél a legelterjedtebb alternatív böngészők így is nagyságrendekkel gyorsabbak:

function isPrime(num) {
    for (var i = 2; i <= Math.sqrt(num); i += 1) {
        if (num % i == 0) {
            return false;
        }
    
        return true;
    }
}

var num = 123456789, count = 0, start, end;

start = new Date();

while (count < 3000) {
    if (isPrime(++num)) {
        ++count;
    }
}

end = new Date();

alert((end - start) / 1000.0 + ' (' + num + ')');
A mérés eredményei diagramon

Természetesen e két kódból nem lehet messzemenő következtetéseket levonni, de megvan annak a lehetősége, hogy kliens oldalon hamarabb végzünk.

Azonban van itt egy bökkenő, és ez a nyomósabb ok, ami miatt nem írunk komolyabb számítási feladatot JavaScriptben: a HTML oldal megjelenítése alapvetően egy szálon fut, így amíg számítást végzünk, semmi más nem történhet a böngészőben. A klasszikus JavaScript programban meghívunk egy függvényt, megkapjuk az eredményét és megjelenítjük: showResult(i, countPrimes(from, to)); Megszámoljuk, hogy hány darab prímszám található from és to között, majd az i-edik cellában jelezzük, hogy kész.

A mellékelt oldalon a Classic gombra kattintva elkezd számolni a böngésző, és (kellően nagy szám esetén) az animáció megáll, de még a gomb is benyomva marad. A számolás végeztével láthatjuk az eredményt, és minden megy tovább – de addigra a felhasználó rég kiábrándult belőlünk. Ha a közben valamilyen módosítást végzünk a DOM-on, az is bekerül a várakozási sorba. Ebben jelenleg egyedüli kivétel az Opera: bár itt is megáll az animáció, de a DOM-on végzett módosítások azonnal megjelennek. A jövőben a WebKit2 alapú böngészők is így fognak viselkedni.

Ezt a valóban hátrányos viselkedést kiküszöbölendő a Google a Gears részeként biztosította a WorkerPool használatának lehetőségét, aminek a bemutatására nem térnék ki, hiszen ezt szabványosítva alkotta meg a WHATWG a HTML5 ajánlás részeként a Web Worker API-t, melynek segítségével háttérfolyamatokat indíthatunk a böngészőben. Egy folyamat üzenetküldéssel kommunikálhat a szülőjével és a gyermekeivel, osztott memória használatára nincs lehetőség. Workert elindítani a forrásfájl megadásával lehetséges, beállíthatjuk, hogy mi történjen üzenet vagy hiba esetén, és természetesen küldhetünk üzenetet (bemutató mellékelve):

var worker = new Worker('hello.js');

worker.onmessage = function(event) {
    alert('Message: ' + event.data);
};

worker.onerror = function(event) {
    alert('Error: ' + event.message );
};

worker.postMessage('world');

A forrásfájl sem bonyolultabb:

onmessage = function(event) {
var name = event.data;

setTimeout(function() {  
    postMessage('Hello ' + name + '!');  
}, 1000);

throw 'Something happened...';
};

A worker oldalon lehetőségünk van további forrásfájlok betöltésére az importSrcipts(file1, file2, …) paranccsal. Erre szükségünk is lesz, hiszen a worker nem látja a szülő folyamatban betöltött moduljainkat. Sőt, mivel itt nem végezhetünk semmilyen DOM műveletet, window és document objektumunk sincs. Ellenben van XMLHttpRequest, így a hálózati kommunikáció rendelkezésünkre áll.

Böngészők: jelenleg a Chrome, a Firefox és a Safari támogatja teljes mértékben. A szabvány szerint szöveges üzeneteket küldhetünk, Firefox esetén bármi mást is, amit JSON-ná tud alakítani. Az Opera és az Internet Explorer nem támogatja, ez utóbbi esetben lehetséges a WorkerPool használata, azonban a két API nem kompatibilis egymással.

Workereket, és a workeren belül újabb workereket elméletileg korlátlan számban indíthatunk, a Mozilla MDC oldalán Fibonaccit számolunk, a rekurzív hívás helyett újabb worker indításával. (A cikk megírásának pillanatában csak a Firefox alatt lehetet workeren belül újabb workert indítani.)

Felmerül még a skálázhatóság kérdése is. Google Chrome esetén minden worker saját processzt kap, Safarinál saját szálon fut, Firefoxban ezt másképpen oldották meg, nem indulnak új szálak (Windows alatt). Emiatt Chrome és Safari esetén megfelelő tervezéssel négymagos gépen négyszer olyan gyorsan számolhatunk. Erről Dennis Forbes írt egy jó cikket. Szintén ő készített egy demó oldalt, amelyen a SunSpider benchmarkokat végezhetjük el workerek segítségével.

De mit is ér a workerben lévő lehetőségeket kihasználó kód, ha az Internet Explorerben nem fut, vagy csak nagyon lassan, és a böngészőt blokkolva végez? Ilyen tekintetben meg kell különböztetünk a weboldalakat és a webalkalmazásokat. Weboldalaknál jelenleg sem végzünk komoly számításokat, még szerver oldalon sem, hiszen ez hamar túlterhelést okozna. A workerek segítségével támogató böngésző esetén átvehetünk egyes feladatokat, úgy, hogy a nem támogató böngészővel érkezőknek nem romlik az élményük. (És talán váltani is fognak, ha a fejlesztő számára kényelmesebb böngésző használata számukra is előnyökkel jár.) Ám nem szabad megfeledkezni arról, mi van, ha a kliens egy netbookkal jön: ha hússzor gyorsabb a szerver alatt a vas, és kétszer lassabb az alkalmazott nyelv, akkor tíz felhasználó párhuzamos kiszolgálása esetén már gyorsabb, ha a kliens oldalon számolok (nem beszélve a hálózati kommunikáció miatti késleltetésről). Alkalmazásoknál megtehetjük, hogy a felhasználót megkérjük a futtató környezet telepítésére – ahogyan Java vagy .NET esetén –, portable böngészők használatával még rendszergazdai jog sem kell hozzá.

(A cikk ikonjához a NIOSH Construction workers not wearing fall protection equipment c. fotóját használtuk fel.)

 
presidento arcképe
presidento
FARKAS Máté, lelkes fejlesztő, egykori rendszeres előadója a budapest.js meetupoknak. Szereti a kihívást és szeret nála profibbakkal együtt dolgozni.
1

XUL

janoszen · 2010. Ápr. 26. (H), 19.29
Én XUL alatt próbáltam web workereket használni a chipkártya leolvasó socket szerveremhez, viszont szomorúan kellett konstatálnom hogy a Gecko implementációban a szülő threadből semmi nem érhető el (talán még a window elemet is beleértve). Innentől kezdve sajnos tényleg csak nyers számításra használhatóak. Remélem, ez azért a jövőben változni fog.
2

Nem fog

presidento · 2010. Ápr. 26. (H), 20.14
Nem valószínű, hogy fog változni, a szálbiztos programozás miatt van így. Ugyanis ezzel a megoldással minden Javascript kód atomi lefutású az elindulástól a számítás végéig.

Valóban nincsen osztott memória terület. De ha a valóban számításigényes kódot teszed át egy (néhány) worker-be, valószínű, hogy az üzenet küldés nem okoz nagy overhead-et, így a szükséges adatokat mindig megkaphatod a szülőtől. (Egyébként szerintem is kényelmesebb lenne, ha legalább olvasási elérhetőség lenne…)

A SharedWorker segítségével majd egy workerhez több „szülő” is csatlakozhat. (Tudtommal jelenleg egy böngésző sem támogatja.)
3

Nem megy

janoszen · 2010. Ápr. 26. (H), 20.44
Sajnos pont ez volt a baj úgyhogy a végső megoldás socket listener lett a fájl nyitás helyett. Van a XULnak is még hova fejlődnie.
4

Hasznos

zzrek · 2010. Ápr. 27. (K), 09.25
Egy webalkalmazásban van szükségem kliensoldali nyers erőre, de a workert még nem próbáltam ki, mert az IE nem támogatja. Most azonban ismét elgondolkodtam rajta:
- gyorsabb is a workerben futtatott kód, vagy "csak" független?
- az iframe-ben futtatott kód is megállítja a szülőablak folyamatait?
- nehéz lenne-e egy olyan illesztőt írni, amelyik az adott kódot workerben futtatja, ha lehetséges, és ha nem, akkor workerpoolt használ, vagy ha az sem, akkor csak simán elindítja (esetleg egy iframe-ben) (?)
(Ezeket a kérdéseket magamnak tettem fel, kipróbálgatom nemsokára)
5

Válaszok

presidento · 2010. Ápr. 27. (K), 10.08
- A Javascript motor ugyanaz, ezért nem gyorsabb (kivéve WorkerPool?)
- Blokkolja. Firefox-nál még a teljesen függetlenül megnyitott másik lapot is.
- Attól függ, mennyire akarsz kompatibilis lenni a különböző APIkkal. A JSWorker ezt akarta megvalósítani, de saját API-t vezetett be, és abbahagyták a fejlesztését. Az IE worker csak emulálná, de az importScripts fájlokat a globális névtérbe teszi. Én is készítek egyet, ez már jobb az iframe-es dolgokban, de még fejlesztés alatt áll.

A Google azt javasolta, hogy használjuk inkább a HTML5 ajánlásait, mint a Gears-t (persze, Chrome-ban, ahol mindkettő működik jelenleg). Nem tudom, mennyire elterjedt az IE + Gears kombó, megéri-e vele foglalkozni…
6

Köszönöm

Kevlar · 2010. Ápr. 27. (K), 10.39
Nagyon klassz volt az előadásod, és a cikk is jó, nekem teljesen újdonság volt ez. Köszönöm!
7

Nagyon köszönjük!

ironwill · 2010. Ápr. 27. (K), 21.46
Szia!

Nagyon hasznos leírás volt. Köszönjük!

üdv, Gábor
u.i: Reméljük mihamarabb támogatni fogja az Explorer is.. az IE22 már egész biztosan.. :)
8

1-2 ötlet

inf3rno · 2010. Május. 1. (Szo), 03.22
Gratulálok a cikkhez, gondolatébresztő volt. Néhány dolog, ami eszembe jutott vele kapcsolatban:

amíg számítást végzünk, semmi más nem történhet a böngészőben

Ez így nem teljesen igaz, meg lehet setTimeout-al oldani a multi thread-et, vagy akár async ajax-al (problémától függően). Persze ez azért közel sem ugyanaz, mint java-ban, ahol készen kapja az ember. (Közben próbálgattam több módszerrel is, de tényleg behal tőle a böngésző :-) visszaszívtam :D)

Nem várt eredmény: azonos gépen futtatva a szerver oldali nyelvek közül csak a C, a Java és a JavaScript (Node.js) képes felvenni a versenyt a modern böngészők JavaScript sebességével.

Javascript-nek egyébként a nagy számokkal elég komoly problémái szoktak lenni... Lehet, hogy kevesebb memóriát foglalnak le a számok, ezáltal kisebb a stack, ahová másolja őket, és ezért gyorsabbak a függvények.
(Olyan nagyon mondjuk nem folytam bele a js belső működésébe, szóval ez csak találgatás...)
9

multiThread

Poetro · 2010. Május. 1. (Szo), 03.39
A JavaScript esetén nem létezik olyan, hogy multithread, ugyanis a setTimeout is akkor fog tudni lefutni, ha a többi már futás alatt levő kód lefutott.

Nézzük a következő kódot:
(function () {
  var i, datum = new Date();
  
  setTimeout(function () {
    alert('multiThread? ' + ((new Date()).valueOf() - datum.valueOf()));
  }, 100);
  for (i=0; i < 10000000; i++) {
    Math.sqrt(i);
  }
})()
Ez nálam 2710-et ír ki mint a különbség milliszekundumban, azaz a 100 milliszekundum helyett majd 3 másodpercig futtot kód, mire végrehajtódott a setTimeout callback-je. Persze ha lerövidítem a ciklust, akkor egyre közelebb lesz a 100 ms-hez, ha pedig meghosszabbítom, akkor pedig egyre távolabb.

A Worker-ekben point az az izgalmas, és hasznos, hogy mindegyik külön szálon fut, azáltal nem blokkolja az aktuális kód futását, mint ahogy normál esetben a JS tenné.
11

Igen

inf3rno · 2010. Május. 1. (Szo), 13.26
Igen, tisztában vagyok vele, hogy rosszul gondoltam, és a böngészőkben csak 1 szál megy.
10

Számok

Poetro · 2010. Május. 1. (Szo), 03.47
Javascript-nek egyébként a nagy számokkal elég komoly problémái szoktak lenni... Lehet, hogy kevesebb memóriát foglalnak le a számok, ezáltal kisebb a stack, ahová másolja őket, és ezért gyorsabbak a függvények.

Nem tudom, tudod-e, de JavaScript-ben egyfajta szám (Number) létezik, és az a hagyományos nyelvekben előforduló Double, vagyis 64 bites lebegőpontos szám. Ezzel a mai számítógépek nagyon gyorsan tudnak számolni, még gyorsabban is általában, mint az egész számokkal.
Másik dolog, ami gyorssá teszi a számokkal való műveleteket, az az, hogy nem kell a különböző szám típusok között konvertálgatni, hanem mivel egyfajta számtípus van, nincs szükség átalakításra.
12

Nem tudtam

inf3rno · 2010. Május. 1. (Szo), 13.27
Nem tudtam.