Node.JS 的特点总共有以下几点
异步 I/O(非阻塞 I/O)
事件驱动
单线程
擅长 I/O 密集型, 不擅长 CPU 密集型
高并发
下面是一道很经典的面试题, 描述了 node 的整体运行机制, 相信很多人都碰到了. 这道题背后的原理就是 Node.JS 代码执行顺序
- setTimeout(function() {
- console.log('4');
- },0)
- setImmediate(function() {
- console.log('5');
- })
- let s = new Promise(function(resolve, reject) {
- console.log('2');
- resolve(true)
- console.log('7')
- })
- s.then(function() {
- console.log('3');
- })
- process.nextTick(function() {
- console.log('6')
- })
- console.log('1');
- // 我电脑的输出结果是 2,7,1,6,3,4,5
1. Node.JS 代码执行顺序(事件循环机制)
Node.JS 的运行机制: Node.JS 主线程主要起一个任务调度的作用. Node.JS 用一个主线程处理所有的请求, 将 I/O 操作交由底下的线程池处理; 在所有主线程任务执行完成后, 主线程处理事件队列. 所以在同步初始化代码执行完成后, Node.JS 会基于事件队列不停的做事件循环. 事实上, Node.JS 运行环境 = 主线程(单线程, 包括事件队列) + 线程池(工作线程池, 执行其他工作 - 多线程)
node 的初始化
初始化 node 环境.
执行输入代码.
执行 process.nextTick 回调.
执行 microtasks.(Promise.then)
进入事件循环
进入 timers 阶段 (定时器阶段: 本阶段执行已经安排的 setTimeout() 和 setInterval() 的回调函数.)
检查 timer 队列是否有到期的 timer 回调, 如果有, 将到期的 timer 回调按照 timerId 升序执行.
检查是否有 process.nextTick 任务, 如果有, 全部执行.
检查是否有 microtask, 如果有, 全部执行.
退出该阶段.
进入 pending IO callbacks 阶段.(对某些系统操作 (如 TCP 错误类型) 执行回调)
检查是否有 pending 的 I/O 回调. 如果有, 执行回调. 如果没有, 退出该阶段.
检查是否有 process.nextTick 任务, 如果有, 全部执行.
检查是否有 microtask, 如果有, 全部执行.
退出该阶段.
进入 idle,prepare 阶段:
仅系统内部使用.
进入 poll 阶段(检索新的 I/O 事件; 执行与 I/O 相关的回调, 除了定时器和关闭的回调函数, 其余都在这里)
首先检查是否存在尚未完成的回调, 如果存在, 那么分两种情况.
第一种情况:
如果有可用回调(可用回调包含到期的定时器还有一些 IO 事件等), 执行所有可用回调.
检查是否有 process.nextTick 回调, 如果有, 全部执行.
检查是否有 microtaks, 如果有, 全部执行.
退出该阶段.
第二种情况:
如果没有可用回调, 执行下一步;
检查是否有 immediate 回调, 如果有, 退出 poll 阶段. 如果没有, 阻塞在此阶段, 等待新的事件通知.
如果不存在尚未完成的回调, 退出 poll 阶段.
进入 check 阶段.(setImmediate() 回调函数在这里执行)
如果有 immediate 回调, 则执行所有 immediate 回调.
检查是否有 process.nextTick 回调, 如果有, 全部执行.
检查是否有 microtaks, 如果有, 全部执行.
退出 check 阶段
进入 closing 阶段.(检测关闭的回调函数, 例如 xx.on('close'))
如果有 immediate 回调, 则执行所有 immediate 回调.
检查是否有 process.nextTick 回调, 如果有, 全部执行.
检查是否有 microtaks, 如果有, 全部执行.
退出 closing 阶段
检查是否有活跃的 handles(定时器, IO 等事件句柄).
如果有, 继续下一轮循环.
如果没有, 结束事件循环, 退出程序.
注: 在主线程执行完和事件循环总共 7 个阶段, 每一个阶段执行完都会调用一遍 process.nextTick 回调, 一遍 microtaks(promise);
2. setImmediate 和 process.nextTick 和 setTimeout
setImmediate(): 事件循环 poll 阶段执行完后执行 setImmediate;
process.nextTick(): 主线程和事件循环每一阶段完成后都会调用;
setTimeout(): 最少经过 n 毫秒后执行的脚本, 受到前一次事件循环时间影响, 实际执行时间为>=n 毫秒
** setTimeout 和 setImmediate 执行顺序问题 **
如果运行的是不属于 I/O 周期 (即主模块) 的以下脚本, 则执行两个计时器的顺序是非确定性的, 因为它受进程性能的约束;
如果你把这两个函数放入一个 I/O 循环内调用, setImmediate 总是被优先调用; I/O 场景推荐使用 setsetImmediate, 因为 setsetImmediate 始终而且是立即执行
3. 对上题的理解
主线程中, console.log 和 promise 的 new 方法在初始化主线程中执行, 他们俩个的输出时间按照先上后下的顺序输出, 他们两个执行完后会立即执行主线程的 process.nextTick, 然后执行 promise.then 方法, 然后是进入事件队列中执行 setTimeout 和 setImmediate. 因为 setTimeout 的
'最少经过 n 毫秒后执行的脚本'特性, 导致无法确定 setTimeout 和 setImmediate 的执行先后顺序, 但如果是在回调函数中, 则必然 setImmediate 先执行, 因为事件循环的阶段中, setImmediate 紧挨着回调函数之后执行, 而 setTimeout 则在下次事件循环中执行.
4. 单线程和多线程
多线程: 服务器为每个客户端请求分配一个线程, 使用同步 I/O, 系统通过线程切换来弥补同步 I/O 调用的时间开销. 比如 Apache 就是这种策略, 由于 I/O 一般都是耗时操作, 因此这种策略很难实现高性能, 但非常简单, 可以实现复杂的交互逻辑.
单线程: 而事实上, 大多数网站的服务器端都不会做太多的计算, 它们接收到请求以后, 把请求交给其它服务来处理(比如读取数据库), 然后等着结果返回, 最后再把结果发给客户端. 因此, Node.JS 针对这一事实采用了单线程模型来处理, 它不会为每个接入请求分配一个线程, 而是用一个主线程处理所有的请求, 然后对 I/O 操作进行异步处理, 避开了创建, 销毁线程以及在线程间切换所需的开销和复杂性.
5. 异步 I/O
IO 操作: IO 操作就是以流的形式, 进行的操作, 比如网络请求, 文件读取写入. IO 操作也就是 input 和 output 的操作.
阻塞 IO: 在调用阻塞 O 时, 应用程序需要等待 IO 完成才能返回结果. 阻塞 IO 的特点: 调用之后一定要等到系统内核层面完成所有操作之后, 调用才结束. 阻塞 O 造成 CUP 等待 IO, 浪费等待时间, CPU 的处理能力不能得到充分利用.
非阻塞 IO: 为了提高性能, 内核提供了非阻塞 IO, 非阻塞 IO 跟阻塞 IO 的差别是调用之后会立即返回. 阻塞 IO 完成整个获取数据的过程, 而非阻塞 IO 则不带数据直接返回, 要获取数据, 还要通过描述符再次读取. 非阻塞 IO 返回之前, node 主线程可以用来处理其他事物, 此时性能提升非常明显.
为什么 node 擅长 I/O 密集型, 不擅长 CPU 密集型: 因为 node 的 I/O 处理中主线程只负责转发, 实际操作在其他线程及线程队列里完成, 所以性能相对较高; 而 CPU 密集则要求 node 的主线程处理, 这时候其余请求只能等待
我的理解: node 的异步 I/O 分为两个阶段, 第一个阶段是主线程调用线程池里的工作线程执行异步操作, 主线程取回对应的描述符, 存储下来, 工作线程执行相关操作取回数据后存储下来, 这一部分在主线程接收到请求后立即完成; 第二个阶段在事件队列里完成, 根据描述符去工作线程里去获取数据, 以提升性能.
6. 高并发
以下是对 Node.JS 高并发的理解, Node.JS 的高并发体现在处理 I/O 的性能上, 而不是 CPU 密集上, 摘录自官网文档
让我们思考这样一种情况: 每个对 web 服务器的请求需要 50 毫秒完成, 而那 50 毫秒中的 45 毫秒是可以异步执行的数据库 I/O. 选择 非阻塞 异步操作可以释放每个请求的 45 毫秒来处理其它请求. 仅仅是选择使用 非阻塞 方法而不是 阻塞 方法, 就是容量上的重大区别.
7. 总结
Node 有两种类型的线程: 一个事件循环线程和 k 个工作线程. 事件循环负责 JavaScript 回调和非阻塞 I/O, 工作线程执行与 C++ 代码对应的, 完成异步请求的任务, 包括阻塞 I/O 和 CPU 密集型工作. 这两种类型的线程一次都只能处理一个活动. 如果任意一个回调或任务需要很长时间, 则运行它的线程将被 阻塞. 如果你的应用程序发起阻塞的回调或任务, 在好的情况下这可能只会导致吞吐量下降(客户端 / 秒), 而在最坏情况下可能会导致完全拒绝服务. 要编写高吞吐量, 防 DoS 攻击的 Web 服务, 您必须确保不管在良性或恶意输入的情况下, 您的事件循环线程和您的工作线程都不会阻塞.
通常意义上, I/O 密集型活动, 如网络 I/O, 文件 I/O,DNS 操作等通常建议放在对外提供网络服务的端口所在的服务内, 剩下的诸如大内容的 crypto,zlib,fs 同步操作, 子进程, JSON 处理, 计算等尽量另起 node 服务或者其他语言服务去进行, 因为这些操作会影响到 node 的主线程的性能和安全性.
参考
Node.JS 事件循环机制
Node.JS 笔记之: 事件驱动, 线程池, 非阻塞, 异常处理等
官网文档
Node.JS 事件循环, 定时器和 process.nextTick()
Node.JS 事件循环
不要阻塞你的事件循环(或是工作线程池
题外话
事实上, 对于 Node.JS 的相关理解更多的收获在于这里, Node.JS 官网指南 https://nodejs.org/zh-cn/docs/guides/ 的中文文档, 以前有点粗心了
来源: https://www.cnblogs.com/zhaowinter/p/11138555.html