本文已参与「新人创作礼」活动,一起开启掘金创作之路。
I/O操作
I/O操作是啥?个人理解通俗简单点讲就是除了CPU资源和寄存器(一般是也只是少量占用),要和占用计算机系统(指硬件层面)中除了CPU以外的其他计算机部分(如内存,磁盘,显卡等设备)的操作。
一般编程过程用得比较多的就是网络I/O(涉及网卡)和文件I/O(涉及磁盘)。
Node.js中的libuv
先总览一下Node.js的架构
libuv为Node.js提供了什么呢?介绍一下。
libuv是一个跨平台的异步I/O的库,一开始就是为了在Node.js使用而开发的。
下面是libuv的架构图:
可以看出,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上的源码。
Nodejs中的事件循环
所以回到Node.js,I/O的操作应该是下面这样的
但是Node.js的事件循环是比上面简化的图更复杂的。
NodeJS中有不止一个事件队列,不同类型的事件在它们自己的队列中排队。
在处理了一个阶段之后,在进入下一个阶段之前,事件循环将处理两个中间队列,直到中间队列中没有剩余的项目。
因为Node.js中的事件队列是由多个的。原生libuv事件循环处理 4 种主要类型的队列。
- 过期定时器和间隔队列——由使用添加的过期定时器的回调
setTimeout或使用添加的间隔函数组成setInterval。 - IO 事件队列- 已完成的 IO 事件
- 立即队列
setImmediate- 使用函数添加的回调 - 关闭处理程序队列- 任何
close事件处理程序。
除了这 4 个主队列之外,还有 2 个队列。这些队列不是libuv本身的一部分,而是NodeJS 的一部分。他们是,
- Next Ticks Queue
process.nextTick- 使用函数添加的回调 - 其他微任务队列——包括其他微任务,例如已解决的承诺回调
所以Node.js中的事件循环应该是这样的。
我们先只看图中红色部分。红色表示的就是上面所说的中间队列,一旦一个阶段完成,事件循环将检查这两个中间队列是否有任何可用项目。如果中间队列中有任何可用的项目,事件循环将立即开始处理它们,直到两个直接队列被清空。一旦它们为空,事件循环将继续到下一阶段。
总结
Node.js的非阻塞I/O是通过libuv的事件循环机制,poll阶段是调用系统提供的Event Demultiplexer(多路选择器)实现。I/O事件进到队列后,会作为宏任务,在Node.js的事件循环机制中被处理。
Reference: