Freewind @ Thoughtworks scala java javascript dart 工具 编程实践 月结 math python english [comments admin] [feed]

(2012-05-14) Nodejs: 异步?!我要同步!!!

广告: 云梯:翻墙vpn (省10元) 土行孙:科研用户翻墙http proxy (有优惠)

Nodejs是一个事件驱动、异步非阻塞的Javascript平台。异步编程模型是它的主要特色,所以nodejs众多的第三方模块都提供了异步api。

这种方式有很多优点,但在某些情况下,会造成以下问题:

  1. 代码多层嵌套,难写难读2. 控制异步调用的先后顺序,很麻烦

具体如何这里就不多说了,相信每个使用过的人都深有感触,不然怎么会有那么多的模块存在的目的就是为了改善这一点呢?

  1. http://altjs.org/ 见Synchronous to Asynchronous (CPS)一节2. https://github.com/joyent/node/wiki/modules#wiki-async-flow

这些通过三种方式来实现:

  1. 通过nodejs的本身设施实现,如async,q,node-seq,step等2. 通过语言扩展,定义一些特有的关键字如await等,如tamejs,jscex。可惜多了一步编译3. 通过node-fibers项目提供的纤程,如fibrous,common-node,streamline等

经过近两天的查看与试用,我对其中几个比较感兴趣。我的标准是:

  1. 示例代码易读、易理解2. 在不同的环境中都能稳定运行(如mocha中)3. 能方便与已有模块的异步api一起使用

下面一一介绍。

Tamejs

https://github.com/maxtaco/tamejs

tamejs不是纯js,因为它增加了两个特有的东西:await/defer。通常把文件保存为.tjs,在运行前需要先编译为.js(也可以通过require某些库在运行期进行)。这里给一个代码示例:

for (var i = 0; i < 10; i++) {

await { setTimeout (defer (), 100); }       
console.log ("hello");       

}

代码看起来相当简洁,为了这个,我宁愿多一个编译步骤!

看看它转换的js代码是什么样的:

var tame = require('tamejs').runtime;
var tame_defer_cb = null;
var
tame_fn_0 = function (__tame_k) {

tame.setActiveCb (__tame_defer_cb);       
var __tame_k_implicit = {};       
var i = 0;       
var __tame_fn_1 = function (__tame_k) {       
    tame.setActiveCb (__tame_defer_cb);       
    var __tame_fn_2 = function (__tame_k) {       
        tame.setActiveCb (__tame_defer_cb);       
        i ++       
        tame.callChain([__tame_fn_1, __tame_k]);       
        tame.setActiveCb (null);       
    };       
    __tame_k_implicit.k_break = __tame_k;       
    __tame_k_implicit.k_continue = function() { __tame_fn_2(__tame_k); };       
    if (i < 10) {       
        var __tame_fn_3 = function (__tame_k) {       
            tame.setActiveCb (__tame_defer_cb);       
            var __tame_fn_4 = function (__tame_k) {       
                tame.setActiveCb (__tame_defer_cb);       
                var __tame_defers = new tame.Deferrals (__tame_k);       
                var __tame_fn_5 = function (__tame_k) {       
                    tame.setActiveCb (__tame_defer_cb);       
                    setTimeout (       
                    __tame_defers.defer ( {       
                        parent_cb : __tame_defer_cb,       
                        line : 2,       
                        file : "d.tjs"       
                    } )       
                    , 100 ) ;       
                    tame.callChain([__tame_k]);       
                    tame.setActiveCb (null);       
                };       
                __tame_fn_5(tame.end);       
                __tame_defers._fulfill();       
                tame.setActiveCb (null);       
            };       
            var __tame_fn_6 = function (__tame_k) {       
                tame.setActiveCb (__tame_defer_cb);       
                console . log ( "hello" ) ;       
                tame.callChain([__tame_k]);       
                tame.setActiveCb (null);       
            };       
            tame.callChain([__tame_fn_4, __tame_fn_6, __tame_k]);       
            tame.setActiveCb (null);       
        };       
        tame.callChain([__tame_fn_3, __tame_fn_2, __tame_k]);       
    } else {       
        tame.callChain([__tame_k]);       
    }       
    tame.setActiveCb (null);       
};       
tame.callChain([__tame_fn_1, __tame_k]);       
tame.setActiveCb (null);       

};
__tame_fn_0 (tame.end);

看起来还是有点吓人的,不容易啊不容易。

非常可惜的是,在mocha中无法正常使用。看下面这段代码:

require('should');

function inc(n, callback) {
setTimeout(function() {

console.log('### inc: ' + n);       
callback(n+1);       

}, 1000);
};

describe('test', function(){
it('show ok with tamejs', function(){

 console.log('### testing ...');       
 var result;       
 await { inc(1, defer(result)); }       
 console.log('result: ' + result);       
 result.should.equal(3);       

});
});

不知为什么,提示测试通过,实际上inc函数完全没有运行。

提了个bug: https://github.com/maxtaco/tamejs/issues/30,希望能早日解决。

Common Node

https://github.com/olegp/common-node

Common node基于node-fibers,提供了同步风格的代码,但在内部利用了纤程,不会阻塞主线程。既可以使用同步风格的代码,又不会影响性能,看起来很不错。

这里有一个性能对比图,可见common-node的同步风格,对于性能的影响还是很小的:

image

common-node的定位应该与node是相同的,但在编程风格上正好相反,它提倡使用“同步”。也因此,对于已有的异步api的模块,它不能很好地直接使用,需要专门为它提供一个修改版。从README中可以看到,目前已经有一些项目支持common-node。可惜相比nodejs庞大的模块库来说,还是可怜了,所以common-node的发展前景还是有些不明朗。

当支持它的模块更多一些之后,也许会有更多人来尝试它的。

Fibrous

(修改:在实际使用中,发现该库与很多其它的库有冲突,出现各种各样的错误,故不再推荐。现在使用async替代,见下段)

https://github.com/goodeggs/fibrous

Fibrous将给每个对象增加一个sync和feature属性,给原有的各异步方法提供一个同步版。因为它也基于node-fibers,所以这个同步函数实际上将在一个纤程中执行,不会阻塞主线程。

它的代码写起来比较简洁,看示例:

var fibrous = require('fibrous');
var crypto = require('crypto');

var random = fibrous(function(length) {

var buf = crypto.sync.randomBytes(length);       
return buf.toString('hex');       

});

random(4,function(err, result) {

console.log(result);       

});

如果我们的代码被fibrous()包着,则可在内部调用其它对象的.sync中的某方法(该方法是原异步方法的同步版本),不需要传入回调函数,直接以返回值形式拿到。

如果我们的代码没有被fibrous包着,则还得老老实实传回调。但这不是大问题,因为大多数情况下,我们都可以用fibrous包起来。

再看看它与mocha结合使用:

require('should');

var fibrous = require('fibrous');

function inc(n, callback) {

setTimeout(function() {
    console.log('### inc: ' + n);
    callback(null, n+1);
}, 1000);

};

describe('test', function(){

it('show ok with tamejs', fibrous(function(){
    var x = inc.sync.call(null, 5);
    console.log('x:'+x);
}));

});```

注意it('..', fibrous(..)),可以直接把测试部分给包起来,内部就可以使用inc的sync版了。执行这个测试,将正确打印出:

x:6

使用fibrous,虽然对js中的对象有些侵入(增加了sync和futrue属性),但是很多时候,可以让我们的代码更加简洁,这个代价是我完全可以承受的。我将在以后更多的情况下使用它,看看会不会有其它问题。

暂时不用再纠结于异步同步的问题了,因为所有能试的方法我基本上都试了一遍,如果连fibrous也不行,那我也就只能放弃,强迫自己去写回调了。

Async

https://github.com/caolan/async

前面说中了,fibrous在使用中,与很多库有冲突,无奈移除,只好再硬着头皮写嵌套。期间纠结得想换语言,直到用了async。

async设计得巧妙又周到,考虑到了很多场景,能够很大程度上消除嵌套。比如说三种常见的情况:

  1. 多个异步函数并行执行,并且在全部执行完以后进行某个操作:parallel2. 多个异步函数依次执行,函数之间没有数据传递:series3. 多个异步函数嵌套执行,每个外层都要把数据传给内层:waterfall

另外还有7种:)

还有一种常见场景,需要对一个数组中的每一个元素进行异步操作,这时又提供了十种操作:

  1. forEach2. map3. filter4. reject5. reduce6. detect7. sortBy8. some9. every10. concat

详细的使用方法,我正在边学习边写示例,发到github上:https://github.com/freewind/async_demo

comments powered by Disqus