nodejs笔记-异步I/O

1.为何要使用异步I/O

1.1 用户体验

浏览器中的Javascripts是在单线程上执行的,而且和UI渲染公用一个线程。这就意味着在执行Javascript时候UI的渲染和响应是出于停滞的状态,若是脚本执行时间超过100ms用户就能感觉到页面卡顿。在B/S模型中若是经过同步方式获取服务器资源Javascript须要等待资源的返回,这段时间UI将会停顿不响应交互。而采用异步方式请求资源的同时Javascript和UI渲染能够继续执行。前端

经过异步执行能够消除UI阻塞现象,可是获取资源速度取决于服务器的响应,假设有这么个场景,获取两个资源数据:node

get('json_a');//须要消耗时间M
get('json_b');//须要消耗时间N
复制代码

若是采用同步方式获取资源的时间为M+N,若是采用异步方式时间则是max(M,N)。随着网站的扩大,数据将会分布在不一样服务器上,分布式也将意味着M与N的值会线性增加。同步与异步的耗时差距也会变大。web

1.2 资源的分配

假设一组互不先关的任务须要执行,主流方法有两种:编程

  • 单线程串行依次执行
  • 多线程并行完成

若是建立多线程的开销小于并行执行,那么多线程的方式是首选。多线程的代价在于建立线程和执行线程时的上下文切换。在复杂业务中多线程须要面临锁、状态同步问题。优点在于多线程在多核CPU上能够提高CPU利用率。json

单线程串行执行缺点在于性能,任意一个任务略慢都会影响下一个执行。一般I/O与CPU计算之间是能够并行进行的,可是同步编程致使I/O的进行会让后续任务等待,形成资源浪费。windows

Node在二者之间作出了本身的方案:利用单线程,远离多线程死锁、状态同步问题;利用异步I/O,让单线程远离阻塞,更好的利用CPU。数组

为了弥补单线程没法利用多核CPU缺点,Node提供了相似前端浏览器的Web Workers的子进程,子进程能够经过工做进程高效的利用CPU和I/O。浏览器

异步I/O调用示意图
[异步I/O调用示意图]

2.异步I/O实现

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

操做系统内核对于I/O只有两种方式:阻塞和非阻塞。调用阻塞I/O时,程序须要等待I/O完成才返回结果,如图:bash

调用阻塞I/O的过程

为了提升性能,内核提供了非阻塞I/O,非阻塞I/O调用以后会马上返回,如图:服务器

调用非阻塞I/O的过程

非阻塞I/O返回后,完整的I/O并无完成,当即返回的不是业务层指望的数据,仅仅是当前调用状态。为了获取完整的数据,应用须要反复调用I/O操做来确认是否完成。这种反复调用判断操做是否完成的计算叫作 轮询

现存的轮询技术主要有这些:

  1. read
    最原始的一种方式,经过反复调用I/O状态来完成数据读取,在获取最终数据前,CPU一直耗用在等待是,示意图:

经过read进行轮询的示意图
2. select 在read基础上的改进方案,经过文件描述符上的事件状态来进行判断,select轮询有一个限制,它采用一个1024长度的数组来保存储存状态,因此它最多能够检查1024个文件描述符,示意图:

经过select进行轮询示意图
3. poll 采用链表的方式来避免数组长度限制,能避免不须要的检查。当文件描述符多时,性能仍是十分低下,于select类似,性能有所改善,如图:

经过poll进行轮询示意图
4. epoll Linux下效率最高的I/O事件通知机制,进入轮询时若是没有检查到I/O事件,将会进行休眠,直到事件将他唤醒。利用的事件通知、执行回调方式,而不是遍历查询,因此不会浪费CPU,执行效率比较高。示意图:

经过epoll进行轮询示意图
5. kqueue 与epoll相似,只存在FreeBSD系统下。

2.2 理想的非阻塞异步I/O

指望的完美异步I/O应该是程序发起非阻塞调用,无需经过遍历或者事件唤醒等轮询方式,能够进行下一个任务,只须要在I/O完成后经过信号或回调将数据传递给应用程序,示意图:

理想异步I/O示意图

2.3现实的异步I/O

经过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术完成数据获取。让一个线程进行处理计算,经过线程之间的通信将I/O获得的数据进行传递,实现异步I/O,示意图:

异步I/O

最初Node在*nix平台下采用libeio配合libev实现I/O异步I/O,Node v0.9.3中,自行实现了线程池完成异步I/O。
windows下经过IOCP来实现(实现原理仍然是线程池,只是由系统内核接受管理)。

windows和*nix平台的差别,Node提供了libuv做为封装,兼容性判断由这一层完成,Node编译期间会判断平台条件。

3.Node的异步I/O

3.1事件循环

启动Node时会建立一个相似while(true)的循环,每执行一次循环过程称之为Tick。每一个Tick的过程就是检查是否有待处理事件,若是有,就读取出事件及其相关的回调函数,若是存在关联的回调函数,就执行。而后加入下一个循环,若是再也不有事件处理就退出进程。如图:

Tick流程图

3.2观察者

在每一个Tick过程当中,怎么判断是否有事件须要处理呢?,这里引入了观察者概念。
每一个事件循环中有一个或多个观察者,而判断是否有事件要处理的过程就是向观察者询问是否须要处理事件。

3.3请求对象

Javascript发起调用到内核执行完I/O操做的过程当中,存在一种中间产物,叫作请求对象。
以fs.open()为例:

fs.open = function(path, flags, mode, callback) {
    //...
    binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback);
}
复制代码

fs.open()是根据指定路径和参数打开一个文件,从而获取一个文件描述符,这是后续全部I/O操做的初始操做。
Javascript层面的代码调用C++核心模块进行下层操做。示意图:

调用示意图
实际上调用了uv_fs_open()方法。在调用过程当中建立了一个FSReqWrap请求对象。从Javascriptc层传入的参数和当前方法都封装在这个请求对象中,回调函数则被设置在对象的oncomplete_sym属性上:

req_wrap->object_->Set(oncomplete_sym, callback);
复制代码

对象包装完毕,将FSReqWrap对象推入线程池中等待执行。此时Javascript调用当即返回,Javascript线程可继续执行当前任务的后续操做,当前的I/O操做在线程池中等待执行,不论是否是阻塞I/O,的不会影响Javascript线程的后续执行。
请求对象是异步I/O过程的重要中间产物,全部状态都保存在这个对象中,包括送入线程池执行以及I/O操做完毕后的回调处理。

3.4执行回调

线程池中的I/O操做调用完毕后,将获取结果储存在req->result属性上,而后通知IOCP(windows下)告知操做已完成,并归还线程到线程池。
在每次Tick的执行中,它会检查线程池中是否有执行完的请求,若是存在,将请求对象加入I/O观察者列队中,而后将其当作事件处理。
I/O观察者回调函数的行为就是取出请求对象的result属性做为参数而后执行回调,调用Javascript中传入的回调函数,至此,这个I/O流程彻底接受,示意图:

整个异步I/O流程

在Node中除了Javascript是单线程外,Node自身其余都是多线程的,除了用户代码没法并行执行,全部I/O则是能够并行起来的。

4.非I/O的异步API

无关I/O的异步API

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

4.1定时器

setTimeout()与setInterval()与浏览器API一致,分别用于单次和屡次定时执行任务。调用它们时建立的定时器会被插入到定时器观察者内部的一个红黑树中,每次Tick执行会到该红黑树中迭代取出定时器对象,检测是否超时,若是超时则造成一个事件,它的回调函数当即执行。 定时器并不是精确,虽然循环很是快可是若是某一次计算占用循环事件特别多,那么下次循环,它可能已经超时好久了。
setTimeout()的行为图:

setTimeout()的行为

4.2 process.nextTick()

若是须要一个当即异步执行的任务,能够这样调用:

setTimeout(() =>{
    //todo
}, 0);

process.nextTick(() => {
    //todo
})
复制代码

因为定时器须要调用红黑树全部比较浪费性能。process.nextTick()方法比较轻量。每次调用process.nextTick()方法,只会把回调函数放入队列中,在下一轮Tick时取出当即执行。全部process.nextTick()更为高效。

4.3 setImmediate()

setImmediate()与process.nextTick()类似,都是将回调函数延迟执行,process.nextTick()执行回调优先级高于setImmediate()。

process.nextTick(() => {
    console.log('process.nextTick');
})
setImmediate(() => {
    console.log('setImmediate');
})
console.log('正常执行')
//执行结果
正常执行
process.nextTick
setImmediate
复制代码

这是由于事件循环对观察者的检查是有前后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者。
process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。process.nextTick()在每次循环中会将数组的回调函数所有执行完毕,而setImmediate()每轮循环中执行链表中的一个回调函数 (当前运行node版本是windows8.7.0,新版的setImmediate处理回调函数已经改变,在一轮循环中setImmediate中的回调函数被所有执行)
列如:

process.nextTick(() => {
    console.log('nextTick执行1');
})
process.nextTick(() => {
    console.log('nextTick执行2');
})
setImmediate(() => {
    console.log('setImmediate执行1');
    process.nextTick(() => {
    	console.log('插入执行');
    })
})
setImmediate(() => {
    console.log('setImmediate执行2');
})
console.log('正常执行')
//执行结果
正常执行
nextTick执行1
nextTick执行2
setImmediate执行1
setImmediate执行2
插入执行
复制代码

5.事件驱动与高性能服务器

利用Node构建web服务器流程图:

利用Node构建web服务器流程图

服务器模型的经典模型:

  • 同步式
    一次只能处理一个请求,其他请求出于等待状态
  • 每进程/每请求
    为每一个请求建立一个进程,能够同时处理多个请求,不具有高扩展性,系统资源有限。
  • 每线程/每请求 为每一个请求启动一个新线程。比启动新进程轻量,可是高并发的时候内存将很快耗光。(Apache采用这种模式),线程多了后上下文切换频繁消耗资源。

Node采用事件驱动方式无需为每一个请求建立新线程,能够省掉不少开销(Nginx采用与Node相同的事件驱动),即便在大并发的状况下也不受上下文切换开销的影响。

相关文章
相关标签/搜索