标签 node 下的文章

Node学习笔记 ---- 异步编程浅谈(2)

异步编程中会遇到的问题

开篇依然用我生活中的一件事来建立问题模型,然后看看在用程序语言描述模型时会有什么问题。

前一阵子找工作,一般来说流程可以描述为:发简历->等通知->电话面试->1面->2面->成功,而期间任何一步都可能失败(throw error),catch之后一般会重开一个"副本"继续发简历->等通知->电话面试...

一般情况下,由于当时不会得出面试结果,所以可以认为这是一个异步模型。

此时假设我有两个目标:Co.A和Co.B, 并且A比B优先级高,我并不会同时发简历,那么用伪代码描述我的整个求职历程可能这样的:

Me.request({
    target: Co.A,
    success: Me.be_interviewed_by_phone({
        target: Co.A,
        success: Me.first_interview({
            target: Co.A,
            success: Me.secend_interview({
                target: Co.A,
                success: '在Co.A成功求职',
                error: ... // 转入Co.B流程
            })
            error: ... // 转入Co.B流程
        }),
        error: ... // 转入Co.B流程
    }),
    error: Me.request({
        target: Co.B,
        success: ..., // 类似Co.A流程
        error: '求职失败'
    })
})

以上是把人的思维最直接的转换为代码时的一种情况,其中使用很多...省略,否则会更长更繁琐。

这里我们不讨论如何封装针对不同公司的求职过程。可以看出就算把...替换成一个函数,这样的代码依然有两个问题:

  1. 嵌套过深,一般这样的代码被称为"Pyramid of Doom"(恶魔金字塔),看上去就很dirty。
  2. 异常处理较为麻烦,由于error几乎是同一个回调,但却要书写N次,以后维护也很麻烦。

在初期编写Node程序时,我经常遇到这样的问题,甚至问题会被放大、延伸,毕竟业务要比上述模型复杂很多。

Promise/Deferred模式

Promise/Deferred模式是解决上述问题的方案之一,在2009年被Kris Zyp抽象为一个提议草案,发布在CommonJS规范中。

我认为简单一句话,Promise可以让你编写出来的程序如人的思维一样:【第一步】-> 【第二步】-> 【第三步】, 并且捕获异常。

如果按照Promise/Deferred模式去实现上述模型,最终的代码可能是这样:

var request = function(co) {
    return Me.request(co)
             .then(function() {return Me.be_interviewed_by_phone(co)})
             .then(function() {return Me.first_interview(co)})
             .then(function() {return Me.secend_interview(co)})
             .then(function() {console.log('在'+co+'求职成功')});
}

request(Co.A).error(function(){
        return request(Co.B).error(function(){console.log('求职失败')});
    })

可以看出这是一个【流水账】式的代码,使用request函数封装之后代码量也变得极少。

下面我们详细说说Promise/Deferred模式。

目前,CommonJS草案中已经包括Promise/A、Promise/B、Promise/D这些异步模型。由于Promise/A较为常用也较为简单,我们主要来看看这个。

在API定义上,Promise/A很简单,只需要具备then()方法即可。一般来说,then()的方法定义如下:

then(fulfilledHandler, errorHandler, progressHandler)

通过继承Node的events模块,我们可以实现一个简单Promise模块。

var Promise = function() {
    EventEmitter.call(this);
}
util.inherits(Promise, EventEmitter); // util是node自带的工具类

Promise.prototype.then = function(fulfilledHandler, errorHandler, progressHandler) {
    if(typeof fulfilledHandler === "function") {
        this.once('success', fulfilledHandler);
    }
    if(typeof errorHandler === "function") {
        this.once('error', errorHandler);
    }
    if(typeof progressHandler === "function") {
        this.on('progress', progressHandler);
    }
    return this;
}

以上是Promise部分,可以看到它负责把回调函数与事件绑定,起一个"承诺(Promise)"的作用。为了完成整个流程,还需要触发这些回调函数,实现这些功能的对象通常被称为Deferred,即延迟对象。实现如下:

var Deferred = function() {
    this.state = 'unfulfilled';
    this.promise = new Promise();
}

Deferred.prototype.resolve = function(obj) {
    this.state = 'fulfilled';
    this.promise.emit('success', obj);
}

Deferred.prototype.reject = function(obj) {
    this.state = 'failed';
    this.promise.emit('error', obj);
}

Deferred.prototype.progress = function(obj) {
    this.promise.emit('progress', obj);
}

这些代码只是最基础的原理展示,病不能用于实际使用。归根结底还是因为Promise是高级接口,内部使用了EventEmitter这一底层接口,而为了让自己的应用程序Promise化,仍需要进一步包装,十分麻烦,所以一般会利用一些更加成熟的Promise实现方案。

Q模块

Q模块是Promise/A规范的一个实现。Q的defer部分有一个叫makeNodeResolver的prototype,实现代码我就不再贴出,大家可以通过npm install q安装以后查看。

顾名思义,makeNodeResolver返回一个Node风格的回调函数。

假设我们要把fs.readFile包装成支持Promise风格的readFile方法,我们可以通过Q模块这样实现:

var readFile = function(file, encode) {
    var deferred = Q.defer();
    fs.readFile(file, encode, deferred.makeNodeResolver());
    return deferred.promise;
}

然后就可以很方便的调用啦,比如这样:

readFile('foo.txt', 'utf-8').then(function(data){
    // success case
}, function(err){
    // failed case
})

Node学习笔记 ---- 异步编程浅谈(1)

何为异步?

我一直认为,编程和生活是想通的。编程的思想,必然可以从生活中找到相似的地方。前几天在麦当劳吃点餐时发现,这原来就是"异步"的一个小小体现:

点餐完毕之后,我从点餐队列转到等待队列,而前台柜员无需等我的食物送来即可以为下一位顾客点餐。

在这个案例里,异步方式充分地利用前台柜员(Program Interface),使其无需等待后厨(I/O)配餐的时间,快速地为每一个顾客(Operator)服务。可想而知,如果换成同步方式,效率必然降低许多。

"异步"(Asynchronous)这个词已经诞生很久,不过大规模使用我想应该是在Web2.0时期----伴随着Ajax技术的流行。

$.ajax({ url: "test.html", complete: function(){
   console.log('Ajax done');
}});
console.log('In process');

以上是一个jQuery的Ajax实例,执行以后会先输出'In process',然后才是'Ajax done'。这大概是我们最早接触到的最简单的异步编程了。所以有人说最早习惯异步编程的一批程序员,可能就是前段工程师了。

然而异步确实是存在于底层系统的一个概念。异步通过底层系统的信号、消息机制已经有了很广泛的应用。只不过在高级编程语言里并不常见。

就拿"直肠子"PHP来说,它是彻头彻尾的同步编程,使用阻塞I/O来让每一次的数据调用都"即时"返回结果,来帮着这些结果数据可以被下一行代码立刻用到。这确实很符合大部分人的逻辑:我要什么,你给我什么,然后我用它来做些什么。(←大部分业务都是这样实现的。

然而随着云服务的崛起,分布式I/O的流行,阻塞I/O的缺点越来越明显,面对高并发会显得很无力。于是很多适合高并发的语言开始活跃起来,只不过有的强调多线程,比如Erlang、Go;有的依赖异步,比如Node.js

异步的优势

以下是个很常见的场景:

$res1 = getDate('from_db');  // 消费时间为M  
$res2 = getDate('from_api'); // 消费时间为N  
doSomething($res1, $res2);

如果是用上述PHP风格的同步编程手段,最终消费时间为M+N。

如果改成异步, 那则会是max(M, N)。

var res1 = res2 = false;  
getData('from_db', function(res)  {res1 = res; if(res1 && res2) doSomething(res1, res2); });  
getData('from_api', function(res) {res2 = res; if(res1 && res2) doSomething(res1, res2); });

随着业务的复杂性增加,异步的优势会更加明显。当然也可以看出,在不用使用类库框架时,异步编程的代码量明显比同步的多。

系统底层的异步实现

其实针对系统内核而言,系统I/O一般只分阻塞和非阻塞两种。

阻塞I/O可以用下图表示:
barrage io.jpg

非阻塞I/O与阻塞I/O的差别在于:调用之后不需要等待,而是立刻返回。CPU的时间片可以用来处理其他事务。
由于立刻返回的并不是数据,而且调用的状态符,所以为了取的数据就需要用轮询重复调用I/O确认是否完成,所以非阻塞I/O的难点在于轮询依然消耗CPU时间片。

目前系统上所使用的轮询技术,一般有如下几种:

  • read。 最原始、性能最低。重复调用检查I/O状态完成完整数据读取,在得到最终数据前,CPU一直耗在等待上。
  • select。 read的改进版,通过文件描述符的事件状态判断,并且使用1024长度的数据来存储状态。
  • poll。 select改进版,使用链表方式避免select的数组长度限制,但在文件描述符很多的时候,性能低下。
  • epoll。 Linux下效率最高的I/O事件通知机制,在进入轮询是如果没有检测到I/O事件,就会进入休眠,直到事件发生将它唤醒,而不需要遍历查询,所以不需要浪费CPU。可以如下图表示:
    epoll.jpg

尽管epoll已经利用了事件来降低CPU的消耗,但由于休眠期间CPU几乎是闲置的,降低了当前线程的CPU利用率,所以依然算不上理想中的异步I/O。

理想中的异步I/O

我们可以根据高级层面的应用实现来推测底层需求,描述理想中的异步I/O:应用程序发起非阻塞调用,无需通过遍历或者事件唤醒等轮询方式,可以直接处理下一个调用,只需等I/O完成后通过信号或回调讲数据传输给应用程序即可。可以如图表示:

ideal async io.jpg

其实linux存在这样的一种方式,原生提供异步I/O方式(AIO),但缺陷是:在存在Linux下,仅支持内核I/O中的O_DIRECT方式读取,导致不能利用缓存。

现实中的异步I/O

现实中的异步I/O实际上使用线程池的方法实现:让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程通讯讲I/O得到的数据进行传递。表示如下:

real async io.jpg

在Node v0.9.3之前,Node自己就是用Marc Alexander Lehmann实现的异步I/O库:libeio。在这之后,Node自己通过线程池完成了异步I/O。

参考资源

《使用异步 I/O 大大提高应用程序的性能》
https://www.ibm.com/developerworks/cn/linux/l-async/

《linux AIO (异步IO) 那点事儿》
http://cnodejs.org/topic/4f16442ccae1f4aa270010a7/

《深入浅出Node.js(五):初探Node.js的异步I/O实现》
http://www.infoq.com/cn/articles/nodejs-asynchronous-io

EOF