Node.js中非阻塞I/O的实现

294 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

I/O操作

I/O操作是啥?个人理解通俗简单点讲就是除了CPU资源和寄存器(一般是也只是少量占用),要和占用计算机系统(指硬件层面)中除了CPU以外的其他计算机部分(如内存,磁盘,显卡等设备)的操作。

一般编程过程用得比较多的就是网络I/O(涉及网卡)和文件I/O(涉及磁盘)。

Node.js中的libuv

先总览一下Node.js的架构

image.png

libuv为Node.js提供了什么呢?介绍一下。

libuv是一个跨平台的异步I/O的库,一开始就是为了在Node.js使用而开发的。

下面是libuv的架构图:

image.png

可以看出,libuv之所以能够跨平台,因为用的还是操作系统中提供的Event Demultiplexer(多路选择器/多路复用器),就是图中绿色部分。这是一种Reactor Pattern设计模式。

可是一些CPU-intensive的操作并不能转化为操作系统异步I/O的操作,这部分操作Node.js是用线程池去处理的。例如Node.js提供的crypto functions和zlib async functions。

面对常见问题:Node.js是单线程还是多线程?

答案应该是,Node.js和event loop都是各自运行在一个线程里的(单线程),但是Node.js的一些阻塞操作是用到多线程的。

事件循环模型

libuv的事件循环模型

下图就是libuv进行时间循环是如何工作的,如果想看代码可以看github上的源码

image.png

Nodejs中的事件循环

所以回到Node.js,I/O的操作应该是下面这样的

image.png

但是Node.js的事件循环是比上面简化的图更复杂的。

NodeJS中有不止一个事件队列,不同类型的事件在它们自己的队列中排队。

在处理了一个阶段之后,在进入下一个阶段之前,事件循环将处理两个中间队列,直到中间队列中没有剩余的项目。

因为Node.js中的事件队列是由多个的。原生libuv事件循环处理 4 种主要类型的队列。

  • 过期定时器和间隔队列——由使用添加的过期定时器的回调setTimeout或使用添加的间隔函数组成setInterval
  • IO 事件队列- 已完成的 IO 事件
  • 立即队列setImmediate- 使用函数添加的回调
  • 关闭处理程序队列- 任何close事件处理程序。

除了这 4 个主队列之外,还有 2 个队列。这些队列不是libuv本身的一部分,而是NodeJS 的一部分。他们是,

  • Next Ticks Queueprocess.nextTick - 使用函数添加的回调
  • 其他微任务队列——包括其他微任务,例如已解决的承诺回调

所以Node.js中的事件循环应该是这样的。

image.png

我们先只看图中红色部分。红色表示的就是上面所说的中间队列,一旦一个阶段完成,事件循环将检查这两个中间队列是否有任何可用项目。如果中间队列中有任何可用的项目,事件循环将立即开始处理它们,直到两个直接队列被清空。一旦它们为空,事件循环将继续到下一阶段。

总结

Node.js的非阻塞I/O是通过libuv的事件循环机制,poll阶段是调用系统提供的Event Demultiplexer(多路选择器)实现。I/O事件进到队列后,会作为宏任务,在Node.js的事件循环机制中被处理。

Reference:

究竟什么是I/O操作呢?

how-nodejs-handles-io

Node JS Internal Architecture

Why does node.js scale? Libuv & epoll & fcntl

event loop and the big picture nodejs event loop