异步I/O:

在我们写前端代码的时候,经常会遇到的异步的一些操作,比如ajax,setTimeout,浏览器事件等,虽然js执行是单线程的,但是浏览器却是有多个线程的,例如发起ajax请求,浏览器会另起一个http线程。Node类似,js执行为单线程,但是遇到一些I/O操作,提供了异步的方式。

关于同步和异步,请移步我之前写的一篇文章.

这篇文章算是对于《深入浅出Node.js》第三章内容的一个摘录和小结:

在了解Node.js中异步I/O前,先了解下关于阻塞和非阻塞的概念:

  • 阻塞I/O:在Node.js执行过程中,如果遇到了磁盘的读写或网络请求时,一般可能会耗费比较多的处理时间,这个时候操作系统会剥夺Node.js这个线程的控制权,这时Node.js便停止了执行,等待系统内核层面完成所有的操作,当I/O操作完成后,操作系统会再将CPU的控制权返还给Node.js这个线程,然后Node.js继续向后执行代码。这个便是阻塞的I/O

  • 非阻塞I/O:当Node.js遇到耗时的I/O时,会将这个I/O的请求交给操作系统,操作系统会立马返回当前调用的状态,此时Node.js会继续向下执行代码,不会发生等待的情况,但是Node.js会重复调用I/O操作来确认是否完成,即轮询

Node的异步I/O:

事件循环

首先得清楚,Node自身执行的模型----事件循环。在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就去除事件及其相关的回调函数。

异步I/O算是Node的特色,它力求在单线程上将资源分配得更加高效。异步I/O的提出主要是为了满足单线程执行的js,当进行耗时的I/O操作的时候,将原有等待I/O完成的时间分配给其他需要的业务去执行。

观察者

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

请求对象

在一般的(非异步)的回调函数的调用中,函数可以由我们自行去调用:

    var forEach = function(list, callback) {
        for(var i = 0; i < list.length; i++) {
            callback(list[i], i, list);
        }
    }

但是在Node异步I/O过程当中,回调函数并不由开发者来调用。浏览器中的异步事件同理,具体内容请戳我。在js发起调用到内核完成I/O操作的过渡过程当中,存在一种中间产物,叫做请求对象
例如Node.js提供的核心fs模块,要去打开一个文件,fs.open(path, flags, mode, callback)。js调用Node.js的核心模块,核心模块去调用C++核心模块去进行下层的操作。再调用底层代码时,创建了一个请求对象,从js层面传入的参数和callback都被封装在这个请求对象上。其中callback被设置在这个对象的一个属性上。
以下内容是在windows环境下Node.js异步I/O模型
windows下,这个对象被推入线程池中等待执行。此时,js调用立即返回,由js层面发起的异步调用的第一个阶段就此结束。Node.js再次获得了CPU的使用权,Node.js可以继续执行当前任务的后续操作。当前I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到js线程后续执行,如此就达到了异步的目的。

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

执行回调

线程池中的I/O操作调用完毕之后,在每次tick执行过程中,会调用IOCP提供的相关方法检查线程池中是否有执行完的请求,如果会将请求对象加入到I/O观察者队列中,然后将其当做事件处理。

Windows下主要通过IOCP来向系统内核发送I/O调用和从内核获取已完成的I/O操作,配以事件循环,以此完成异步I/O的过程。在Linux下通过epoll实现这个过程,FreeBSD下通过kqueue实现,Solaris下通过Event ports实现。不同的是线程池在windows下由内核(IOCP)直接提供,*nix系列下有libuv执行实现。


苹果小萝卜
5.1k 声望356 粉丝

Github: [链接]