《深入浅出node.js》读书笔记2-1异步I/O

系统的异步I/O实现现状

异步I/O与非阻塞I/O

从计算机内核I/O而言,异步/同步和阻塞/非阻塞实际上是两回事。
操作系统内核对于I/O只有两种方式:

  • 阻塞。调用阻塞I/O时,应用程序需要等待I/O完成才返回。
  • 非阻塞。调用阻塞I/O时,会立即返回。

非阻塞需要通过轮询去确认是否完全完成数据获取。
理想的非阻塞异步I/O = 非阻塞 + 异步 :应用程序发起非阻塞调用,无须通过遍历或者事件唤醒等方式轮询,可以直接处理下一个任务,只需在I/O完成后通过信号或回调将数据传递给应用程序。

Node的异步I/O

事件循环

Node进程启动时,Node便会创建一个类似while(true)的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行他们。然后进入下一个循环,如果不再有事件处理,就退出进程。流程图如下:

观察者

事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件处理。
在Windows下,这个循环基于IOCP创建,而在*nix下则基于多线程创建。

请求对象

从Javascript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它叫请求对象
下面以最简单的fs.open()(windows下)方法为例,探索Node与底层之间如何执行异步I/O调用以及回调函数究竟是如何被调用执行:

1
2
3
4
5
6
7
fs.open = function(path, flags, mode, callback) { 
// ...
binding.open(pathModule._makeLong(path),
stringToFlags(flags),
mode,
callback);
};

调用示意图如下:

在libuv封装层中,uv_fs_open()的调用过程中,我们创建了一个FSReqWrap请求对象。从Javascript层传入的参数和回调函数都被封装在这个请求对象中。对象包装完毕后,在Windows平台下,则调用QueueUserWorkItem()方法将这个FSReqWrap对象推入线程池中等待执行。
至此,Javascript调用立即返回,由Javascript层面发起的异步调用的第一阶段就此结束。Javascript线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不会影响Javascript线程,如此达到异步的目的。
请求对象是异步I/O过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。

执行回调

组装好请求对象,送入I/O线程池等待执行,这是第一部分,回调通知是第二部分。
线程池中的I/O操作调用完毕之后,会将获取的结果储存在请求对象的result属性上,然后通过IOCP,告知当前对象操作已经完成。
这个过程中,还动用了事件循环的I/O观察者。在每次Tick的执行中,观察者会检查线程池中是否有执行完的请求,如果存在,会将请求对象加入到I/O观察者的队列中,然后将其当做事件处理。
I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出回调函数,然后调用执行,以此达到调用Javascript中传入的回调函数的目的。
至此,整个异步I/O的流程完全结束。

事件循环、观察者、请求对象、I/O线程池这四者共同构建了Node异步I/O模型的基本要素。

非I/O的异步API

setTimeout()setInterval()setImmediate()process.nextTick()

定时器

setTimeout()setInterval()的实现原理与异步I/O比较类似,只是不需要I/O线程池的参与。创建的定时器被插入到定时器观察者内部的一个红黑树中。每次Tick执行时,会从该红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,它的回调函数立即执行。

process.nextTick()、setImmediate()

process.nextTick()setImmediate()十分类似,都是将回调函数延迟执行。但是process.nextTick()属于微任务micro-task。
⚠️注意 :node版本更新到11,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,这点就跟浏览器端一致。

  • 从结果来看,process.nextTick()中的回调函数执行的优先级要高于setImmediate()
  • 在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的回调则是保存在链表中;
  • 行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完,而setImmediate()在每轮循环中执行链表中的一个回调函数。

事件驱动与高性能服务器

从异步实现的原理,可以勾勒出事件驱动的实质:
通过主循环加事件触发的方式来运行程序。
利用Node构建Web服务器,正是在这样一个基础上实现,其流程如图:

3种经典的服务器模型,对比一下它们的优缺点:

  • 同步式:一次只能处理一个请求,并且其余请求都处于等待状态;
  • 每进程/每请求:为每个请求启动一个进程,这样可以处理多个请求,但是它不具备扩展性,因为系统资源不够;
  • 每线程/每请求:为每个请求启动一个线程。线程比进程要轻,但线程也会占用一定内存,当高并发时,内存会很快用光。代表软件Apache。

总结

事件循环是异步实现的核心,node11之后与浏览器中的执行模型几乎完成一致。

liborn wechat
欢迎您扫一扫上面的微信二维码,订阅我的公众号!