node.js: zpracování úloh náročných na čas
Jakkoliv se node.js tváří asynchronně a umožňuje jednoduše psát asynchronní kód, je nutné si uvědomit, že se tak ve většině případů skutečně jenom tváří.
Představme si serverovou aplikaci bežící na node.js. Aplikace čeká na příchozí požadavek na jehož základě vykoná nějakou akci a vrátí odpověď. Dokud je akce nenáročná a/nebo pokud pracuje jenom s I/O, vše funguje jak má. Pokud ovšem potřebujeme vykonat náročnější úlohu, pak se začnou některé požadavky ztrácet...
Jak je to možné? Následující kód napoví:
var util = require('util');
var events = require('events');
function MyAsync()
{
events.EventEmitter.call(this);
}
util.inherits(MyAsync, events.EventEmitter);
MyAsync.prototype.doIt = function(to)
{
var originalTo = to;
while (to > 0) to--;
this.emit('done', originalTo);
}
function doneHandler(to)
{
console.log(to + ' is done');
}
var a = new MyAsync();
a.on('done', doneHandler);
var b = new MyAsync();
b.on('done', doneHandler);
a.doIt(10000);
b.doIt(400);
Jestli čekáte, že se jako první vypíše "400 is done", pak jste se nechali nachytat. Node (stejně jako JavaScript nebo Flash) je pouze single-threaded, tudíž řádek a.doIt(10000);
zastaví program do doby, než akce skončí. Takže i přesto, že je kód napsán s ohledem na asynchronní zpracování, ve výsledku se asynchronně nevykoná.
V případě, že musíte vykonat nějakou časově náročnou úlohu a nemáte/nechcete použít message queue (případně worker), pak můžete použít následující trik s setImmediate
(v předchozích verzích process.nextTick
):
var util = require('util');
var events = require('events');
function MyAsync()
{
events.EventEmitter.call(this);
}
util.inherits(MyAsync, events.EventEmitter);
MyAsync.prototype.inOneTick = 10;
MyAsync.prototype.originalTo = 0;
MyAsync.prototype.doIt = function(to)
{
this.originalTo = to;
this.doItButNotAll(to);
}
MyAsync.prototype.doItButNotAll = function(to)
{
var x = Math.max(to - this.inOneTick, 0);
while (to > x) to--;
if (to === 0) {
this.emit('done', this.originalTo);
} else {
var that = this;
setImmediate(function()
{
that.doItButNotAll(to);
});
}
}
function doneHandler(to)
{
console.log(to + ' is done');
}
var a = new MyAsync();
a.on('done', doneHandler);
var b = new MyAsync();
b.on('done', doneHandler);
a.doIt(10000);
b.doIt(400);
Nyní se kód opravdu vykoná "asynchronně" a jako první do konzole dorazí "400 is done". Slovo asynchronně jsem do uvozovek nenapsal omylem, kód se totiž nestal asynchronním o nic víc než první příklad. Trik je v tom, že se jedna náročná úloha rozloží na x menších, mnohem méně náročných úloh, které se stihnou vykonat v jednom "tiku", takže aplikace zůstane responzivní.
Výše popsaný trik můžete uplatnit také aplikacích na straně klienta, čímž můžete předejít zamrzání prohlížeče.
Já jsem výše popsané řešení použil při vývoji knihovny pro asynchronní dekódování GIFů.