为什么要异步IO

用户体验

如果脚本执行的时间超过100毫秒,用户就会感到页面的卡顿,以为网址停止响应。通过同步的方式获取资源,那么javascript则需要等待资源完全从服务器端获取后才能继续执行,这期间UI将停顿,不响应用户的交互行为;而采用异步请求,在下载资源期间,javascript和UI的执行都不会处于等待状态,可以继续响应用户的交互行为,给用户一个鲜活的页面。

I/O是昂贵的,分布式I/O是更昂贵的

资源分配

问题:在计算机资源中,通常I/O与CPU计算之间是可以并行进行的,但是同步的编程模型导致的问题是,I/O的进行会让后续任务等待,这会造成资源不能被更好的利用。

Node给出了好的解决方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好的利用CPU。

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

我们时常提到的Node是单线程的,这里的单线程仅仅只是JavaScript执行在单线程中而已。在Node中,无论是*nix还是windows平台,内部完成I/O任务的另有线程池。

Node的异步I/O

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

事件循环

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

事件循环是一个典型的生产者/消费者模型

观察者

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

请求对象

请求对象是异步I/O过程中重要的中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。

执行回调(I/O线程池)

每次Tick执行,检查线程池中是否有执行完的请求,如果存在,将请求对象加入到I/O观察者的队列中,然后将其当作事件处理。

I/O观察者回调函数的行为就是取出请求对象的result属性作为参数,取出oncomplete_sym属性作为方法,然后调用执行,这样就能达到调用javascript中传入的回调函数的目的。

注意:除了用户代码无法并行执行外,所有的I/O(磁盘I/O和网络I/O)都是可以并行执行的

Node通过事件驱动的方式处理请求,无须为每一个请求创建额外的对应线程,可以省掉创建线程和销毁线程的开销,同时操作系统在调度任务时因为线程较少,上下文切换的代价很低。使得服务器能够有条不紊地处理请求,即使在大量链接的情况下,也不受线程上下文切换开销的影响,这是Node高性能的一个原因。

流程图

Tick流程图

基于libuv的架构示意图

整个异步I/O流程

基于Node构建Web服务器流程图