node浅析--异步IO和事件循环

1,315 阅读4分钟

前言

异步对于前端来说是老生常谈的话题,同样的,学习node也离不开异步IO与事件循环,之前一直不是很懂,最近正在看相关的文档,顺便总结一下。

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

之前一直不是很理解异步与非阻塞的区别,一直以为是同一回事,看起来都是达到了我们并行I/Ode目的,但实际上,异步/同步和阻塞/非阻塞实际上是两回事。

同步和异步关注的是消息通信机制,所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

显然,阻塞IO容易造成CPU等待浪费,非阻塞IO却需要轮询去确认是否完成全部的数据获取,它会让CPU处理状态判断,是对CPU资源的浪费,下面我们来简单了解下目前轮询的一些方式。

轮询

  • read
    它是一种最原始、性能最低的一种轮询方式,它会重复检查I/O的状态来完成数据的完整读取。在得到最终数据前,CPU一直耗用在I/O状态的重复检查上。如下图所示
    -select
    它是在read的基础上改进的一种方案,通过对文件描述符上的事件状态进行判断。下图是通过select进行轮询的示意图。select轮询具有一个较弱的限制,那就是由于它采用一个1024长度的数组来存储状态,也就是说它最多可以同时检查1024个文件描述符。
    -poll
    poll比select有所改进,采用链表的方式避免数组长度的限制,其次它可以避免不必要的检查。但是文件描述符较多的时候,它的性能是十分低下的。
    -epoll
    该方案是Linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒。它是真实利用了事件通知,执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。

Node的异步I/O

如上图所示,展示的为完整的Node异步I/O流程,总结下来,关键是单线程,事件循环,观察者和I/O线程池,以调用fs.open()为例,大体流程为JS发起调用Node的核心模块,核心模块调用C++的内建模块,内建模块通过libuv进行系统调用,组装好请求对象,送入I/O线程池等待执行,然后通过事件循环观察是否有可执行的回调。

非I/O的异步API

虽然node中涉及到较多的异步I/O,但同时还存在着一些与I/O无关的异步API,像setTimeOut(),setInterval(),setImmediate(),process.nextTick()

那么这些API存在哪些区别呢?

setTimeOut()setInterval()创建的定时器会被插入到定时器观察者内部的一个红黑树中,每次Tick执行时,会从红黑树中迭代取出定时器对象,检查是否超过定时时间,如果超过,就形成一个事件,其回调函数立即执行

setImmediate()process.nextTick()的功能十分相似,都是延迟执行函数,但process.nextTick()的优先级是要高于setImmediate()的。原因是事件循环对观察者的检查是有先后顺序的,process.nextTick()属于idle观察者,setImmediate()属于check观察者,在每一个轮询中,idel观察先于I/O观察,I/O观察先于check观察者。

参考&引用

setImmediate vs nextTick vs setTimeout(fn, 0)

Node.js Event Loop 的理解

Tasks, microtasks, queues and schedules