NodeJS的异步I/O

     这篇博客基于对《深入浅出Node.js》第三章的理解(因为理解的不全面所以只描述看懂的部分)。我认为模块机制和异步I/O是node.js中最重要的部分,尽管可能在很多时候并不会被直接使用。通过这两个部分了解node.js以后,再使用一些应用型功能可能会理解的更多。

     浏览器中javascript执行与UI渲染共用一个线程,所以在浏览器中网络请求多采用异步的方式。原因是我们希望实现并行I/O。并行I/O的意义在于当当前线程需要进行I/O请求时,操作系统可以调度其他线程进行CPU计算,以充分利用资源。

     异步/同步与阻塞/非阻塞是两回事。操作系统内核对于I/O只有两种方式:阻塞与非阻塞。阻塞I/O的调用的结束是以操作系统内核完成所有操作(以读取磁盘文件为例,包括寻道、读取数据、复制数据到内存)为标志,而应用程序也需要等到阻塞I/O调用结束后才返回结果。所以阻塞I/O可能会造成CPU对I/O不必要的等待。对于非阻塞I/O,应用程序在向系统内核发送I/O请求(比如文件读取、网络请求)得到文件描述符后,可以继续执行其他操作。但是在之后的时间里,需要根据文件描述符向系统内核发送请求确认读取数据是否完成,或者由内核通知应用程序数据读取的结束。目前正在使用的一种比较好的方式是当数据读取结束后,由系统内核通知应用程序数据的完成,然后应用程序执行回调。

     这种机制实现的方式是通过多线程。让部分线程通过阻塞I/O或者非阻塞I/O的方式加轮询技术(反复询问内核数据是否读取完毕)来完成数据获取,其他线程进行计算操作,来模拟应用程序与内核之间的通信(实际上是应用程序的I/O线程与内核通信,应用程序的计算线程与I/O线程通信)。如图1所示(图中主线程,我的理解就是应用程序的计算线程)。

     

                                                图1 异步I/O

    *nix与windows操作系统内核都有相应的机制来支持上述异步I/O模型(*nix是自定义线程池,windows是IOCP),node.js通过libuv库屏蔽操作系统差异。所以node.js并不是单线程的,只是javascript执行在单线程中,I/O线程另有线程池。并且用户线程是单线程的,但是I/O线程之间是可以并行的,比如文件I/O与磁盘I/O。在windows中线程池由操作系统提供(iocp),在*nix中由node的libuv模块实现。

   至于node.js对于异步I/O具体的实现,分为事件循环、观察者和请求对象三个环节。

   事件循环:

   事件循环是node.js自身的执行模型。node进程启动时,便会创建一个类似while(true)的循环,每执行一次循环体的过程我们称为一个tick。每个tick中,查看是否有事件待处理。如果有,就取出回调执行。(书中说没有事件了进程就结束了,难道没有事件了node进程就退出了嘛。。。什么叫做没有事件了?)

   观察者:

   在每个tick中,如何判断是否有事件需要处理呢?这里必须要引入的概念是观察者。每个事件循环中有1个或多个观察者,判断是否有事件处理就是通过向观察者询问是否有要处理的事件。观察者分为文件I/O观察者,网络I/O观察者等。我的理解是观察者是node的一部分。

   请求对象:

   在我们通过javascript发起异步请求到内核执行完I/O操作之间,有一个中间产物叫做请求对象。请求参数与请求回调都被写在请求对象中。这个对象由libuv创建(或者操作系统),被推入线程池中等待执行,至此javascript的异步调用已经返回了。当线程池中I/O操作调用完毕以后,会将操作结果写入请求对象中req对象的result属性,并通知操作系统的异步模块(比如在windows中是IOCP)。在每次tick的过程中,观察者会调用操作系统提供的接口检查是否有执行完的操作,如果有,将请求对象加入观察者的执行队列中。这些请求对象被当作事件来处理,I/O观察者将请求对象中的回调取出并执行。至此异步I/O的流程结束。完整流程如图2所示(回调函数是观察者执行的吗?)

                                                 图2 异步I/O流程

   非异步I/O的异步API

   主要谈定时器。定时器的实现也是通过定时器观察者,只是与I/O线程池无关。调用setTimeout或者setInterval时,一个定时器对象被创建,并且被加入到定时器观察者内部的红黑树中,每次tick会取出该对象,查看是否到时。如果超时,则行成事件,回调函数将立即执行。定时器并不总是准确的,如果定时器被设定在10ms之后执行,但是9ms之后有一个任务占用了5ms,则轮到定时器回调执行时就已经过期了4ms(也就是很有可能上次tick时轮到该该定时器时时间未到,本次tick轮到它时已经过期了)。setTimeout 0的机制正在于此。但是setTimeout0不如process.nextTick()精准。

  综上,node通过事件驱动的方式处理请求,而不是为每一个请求创建一个线程。nginx也是事件驱动的工作方式。

  

   

  

   

   

 

   

    

   

posted @ 2016-08-04 21:33  Angela多多  阅读(345)  评论(0编辑  收藏  举报