NodeJS -- 异步I/O

来源:互联网 发布:mac卸载 编辑:程序博客网 时间:2024/05/20 07:16

为什么要异步I/O

Node是面向网络而设计的,因此,异步IO格外重要。下面,我们从两个方面来说。

原因1:用户体验

异步的概念之所以首先在Web中火起来,是因为在浏览器中,JS单线程执行,而且它还和UI渲染公用一个线程。这意味着,JS执行的过程中,UI渲染和响应都是处于停滞状态的。

另一方面,随着网站或应用不断膨胀,数据将会分布到多台服务器上,分布式将会是常态。这也会放大同步和异步在性能方面的差异。异步IO在Node中如此盛行,就是因为:IO是昂贵的,而分布式IO是更加昂贵的。

只有后端能够快速响应资源,前端的体验才能变好。

原因2:资源分配

Node在单线程和多线程的优劣之中给出的方案是:
利用单线程,远离多线死锁、状态同步等问题;利用异步IO,让线程远离阻塞,以更好的使用CPU。

异步IO可以算作是Node的特色,因为它是首个大规模将异步IO应用在应用层上的平台,力求在单线程上将资源分配的更高效。

而为了弥补单线程无法利用多核CPU的缺点,Node提供了类似前端浏览器中WebWorkers的子进程。

这里写图片描述

异步IO实现现状

异步IO与非阻塞IO

从计算机内核IO而言,异步/同步和阻塞/非阻塞是两回事。

这里写图片描述

这里写图片描述

理想的非阻塞异步IO

理想的异步IO应该是应用程序发起非阻塞调用,无须通过遍历或事件唤醒等方式轮询,直接处理下一个任务,只需要在IO完成后通过信号或者回调将数据传递给应用即可。

这里写图片描述

现实的异步IO

多线程的方式

windows下的IOCP就是一个异步IO的方案。它的内部任然是线程池远离,不同之处是这些线程池由系统内核接手管理。

注:
由于windows平台和*nix平台的差异,Node提供了libuv作为抽象封装层,所有平台兼容性的判断都由这一层来完成,并保证上层的Node与下层的自定义线程池及IOCP之间各自独立。Node在编译期间会判断平台条件,选择性编译unix目录或者是win目录下的源文件到目标程序中。

这里写图片描述

另一个需要强调的地方在于,我们时常提到Node是单线程的,这里的单线程仅仅是JS执行在单线程中罢了,在Node中,无论是*nix还是windows平台,内部完成IO任务的另有线程池。

Node的异步IO

事件循环

Node自身的执行模型:事件循环。

在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程,我们称为Tick。流程如如下:

这里写图片描述

观察者

在每个Tick中,如何判断是否有事件需要处理呢?这里必须要引入的概念是观察者。

每个事件循环中,都有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

事件循环是一个典型的生产者/消费者模型。异步IO,网络请求等则事件的生产者,这些事件被传递到对应的观察者哪里,事件循环从观察者哪里取出事件并处理。

在win中,这个循环基于IOCP创建,而在*nix下则基于多线程。

请求对象

Node中的异步IO调用,回调函数不是由开发者调用的。那么从我们发出调用后,到回调函数被执行,中间发生了什么呢?

事实上,从JS发起调用到内核执行完IO操作的过渡过程中,存在一种中间产物,叫做请求对象。

下面我们以最简单的fs.open()方法为例来讲解,探索Node与底层之间是如何执行异步IO调用以及回调函数究竟是如何被调用执行的。

fs.open()的作用是根据指定路径和参数去打开一个文件,从而得到一个文件描述符,这是后续所有IO操作的初始操作。

这里写图片描述

从JS调用Node核心模块,核心模块调用C++内建模块,内建模块通过libuv进行系统调用,这是Node里经典的调用方式。这里libuv作为封装层,有两个平台的实现,实质上是调用了uv_fs_open()方法。在这个方法的调用过程中,我们创建了一个FSReqWrap请求对象。从JS层传入的参数和当前方法都被封装在这个请求对象中,我们最为关注的回调函数则被设置在这个对象的oncomplete_sym属性上。

对象包装完毕后,在win下,调用QueueUserWorkItem方法将这个FSReqWrap对象推入线程池中等待执行。至此,JS调用立即返回,JS线程可以继续执行当前任务的后续操作。当前的IO操作则在线程池中等待执行。不管它是否阻塞IO,都不会影响到JS线程的后续执行。

请求对象是异步IO过程中重要的中间产物。

执行回调

组装好请求对象,送入IO线程池中等待执行,实际上就完成了异步IO的第一部分,回调通知是第二部分。

线程池中的IO操作调用完毕之后,会将获取的结果存储在req->result属性上,然后调用PostQueuedCompletionStatus通知IOCP,告知当前对象操作已经完成。

这个过程中,其实还动用了事件循环的IO观察者。在每次Tick执行中,它会调用IOCP相关的GetQueuedCompletionStatus方法检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到IO观察者的队列中,然后将其当作事件处理。

这里写图片描述

事件循环,观察者,请求对象,IO线程池这四个共同构成了Node异步IO模型的基本要素。

非IO的异步API

定时器

process.nextTick()

示例代码:

process.nextTick = function(callback) {     // on the way out, don't bother.    // it won't get fired anyway    if (process._exiting) return;    if (tickDepth >= process.maxTickDepth)         maxTickWarn();    var tock = { callback: callback };    if (process.domain) tock.domain = process.domain;     nextTickQueue.push(tock);    if (nextTickQueue.length) {        process._needTickCallback();     }};

相比于setTimeout更高效

setImmediate()

事件驱动

事件驱动的实质:通过主循环+事件触发的方式来运行程序。

总结一下:

事件循环是异步实现的核心,它与浏览器中的执行模型基本保持了一致。
Node正是依靠构建了一套完善的高性能异步IO框架,打破了JS在服务器端止步不前的局面。

原创粉丝点击