NodeJs异步IO

日期:2017-07-17       浏览:976

一 异步的应用

对于前端开发人员来说,异步编程再普遍不过了(ajax)。
对于服务端(java)开发人员来说,也存在异步编程,相关编写可参见Java分类下的“java编写异步处理方法提升系统性能”该篇文章,这里也就不放传送门了。
对于移动端所有的触发事件也都是异步的,相关开发人员应该也都是很熟悉的。

二 异步和同步的比较

在java中如果需要发起网络请求,假设我们已经封装好了一个方法,如下:
Log.log("发起网络请求");

JSONObject result1 = HttpUtil.get("http://host:port/uri1");

Log.log("网络请求1返回内容:" + result1.toString());

JSONObject result2 = HttpUtil.get("http://host:port/uri2");

Log.log("网络请求2返回内容:" + result2.toString());

// do something...
以上代码是顺序执行的,也就是说网络情况很差的环境下,后续的操作(do something...)就算与网络请求返回结果无关也必须要等待。
下图说明一下同步IO的执行顺序:
同步IO调用示意图
同步IO调用示意图
所以以上代码的总用时=网络请求1的用时时长+网络请求2的用时时长
下面我们看看使用javascript的异步请求具体情况:
console.log("发起网络请求");

request('http://host:port/uri1', function (error, response, body) {
    if(error) return console.error(error);
    
    console.info('网络请求1返回内容', body);
});

request('http://host:port/uri2', function (error, response, body) {
    if(error) return console.error(error);
    
    console.info('网络请求2返回内容', body);
});

console.log('do something...');
// do something...
以上代码会先输出发起网络请求,然后是do something...,再然后的输出就要看这两个网络请求哪一个最先完成了。
下图说明一下异步IO的执行顺序:
异步IO调用示意图
异步IO调用示意图
以上代码总用时=Max('网络请求1用时时长', '网络请求2用时时长');
以上只是两次IO的比较,如果应用存在大量的IO操作,这个性能的差距就是巨大的,异步IO的优势也就凸显出来了。尤其是在现在微服务盛行的年代,一个应用需要调用多方服务,这时使用异步IO就很有必要了。

三 Node异步IO的实现

众所周知,javascript是单线程执行的,也就是说所有的非IO请求的代码都是在主线程内执行,但是当我们发起IO请求时,该IO调用就不是在主线程执行了,不然主线程就会被阻塞掉,无法响应其它事件。那么在我们发起IO请求时,具体是怎么个执行过程呢?看下图:
异步IO调用
异步IO调用
上图表明了当我们发起IO请求时,调用的是各个不同平台的操作系统内部实现的线程池内的线程。这里的IO请求可不仅仅是读写磁盘文件,在*nix中,将计算机抽象了一层,磁盘文件、硬件、套接字等几乎所有计算机资源都被抽象为文件,文章中说的IO请求就是抽象后的文件。
那么可能有人又会存在疑问了,Node是怎么实现跨平台的,不同的操作系统实现的线程池应该是不同的,这里我们再放一张图:
基于libuv的架构示意图
基于libuv的架构示意图
上图可以看出,Node是基于libuv封装层上运行来实现跨平台兼容的,所有平台兼容性的判断都由这一层来完成,并保证上层的Node与下层的自定义线程池及IOCP之间各自独立。Node在编译期间会判断平台条件,选择性编译unix目录或是win目录下的源文件到目标程序中。这一点就像java是基于jvm运行一样,jvm实现了底层操作系统的差异性。
以上说的都是Node实现异步IO的系统支持,下面我们来具体看下它的内部实现。如下图:
Node实现异步IO的流程
Node实现异步IO的流程
如上图所示,事件循环、观察者、请求对象、IO线程池这四者共同构成了Node异步IO模型的基本要素。
主线程内操作:主线程发起异步IO调用,然后将请求参数(文件路径filePath、回调函数callback等)等信息封装到请求对象上,然后将请求对象放入请求队列,等待线程池给该请求分配可用线程。
线程池内操作:线程池有可用线程,取出请求队列内请求对象,对其分配线程。在分配的线程内执行请求对象中IO操作,执行完成后会将执行结果result封装到请求对象上,并通知线程池IO操作已完成,然后将该线程归还给IOCP线程池。
事件循环内操作:类似于while(true),获取已完成IO操作的IO事件,并触发该事件。相对应的IO观察者会观察到该事件,并获取请求对象,该请求对象上已经封装了请求时的回调函数callback和IO操作的执行结果result,IO观察者取出回调函数和IO执行结果并调用执行callback(error, result)。注:这里说的相对应的IO观察者是针对不同类型会有不同的观察者,例如网络请求IO观察者、文件读写IO观察者等,观察者将事件进行了分类。
事件循环是一个典型的生产者/消费者模型。异步IO、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则是从观察者那里取出事件并处理。
扫码关注有惊喜

(转载本站文章请注明作者和出处 qbian)

暂无评论

Copyright 2016 qbian. All Rights Reserved.

文章目录