主题
Node中js的执行机制
1. 理解Nodejs的多线程
在nodejs代码运行环境中,他为js代码的执行提供了一个主线程,通常我们所说的单线程就是这个主线程,主线程用来执行所有同步代码,但是,Node代码运行环境本身是由 C++ 开发的,在Node内部它依赖了一个叫做 libuv
的 C++ 库,在这个库中它维护了一个线程池,默认情况下,在这个线程池中维护了4个线程,js的异步代码就是在这些线程中执行的,所以说js代码的运行依靠了不止一个线程,所以,js本质上还是多线程的。
其实,不管是在浏览器环境还是在node环境,js的代码执行环境都是多线程的,否则,是无法实现异步代码和同步代码同时执行的
例子:
- 同步代码在主线程执行的例子,会阻塞主线程:
解释:上面的代码是一个同步的 对字符串加密的循环的例子,循环两次;
加密属于复杂的运算过程,它的执行是需要花费时间的,
最终执行结果图如下
- 异步代码在libuv线程中不会阻塞主线程的例子:
解释: 上面的代码是一个异步 加密过程,循环两次
异步代码是在libuv维护的线程池里面的线程去执行,不是主线程,libuv线程池默认有4个线程,这里会在不同的线程去执行,所以返回的时间相近,属于并行操作。
最终执行结果如下:
2. Nodejs事件循环机制EventLoop
2.1 事件循环机制做的是什么事情?
事件循环机制用于管理异步API的回调函数什么时候回到主线程中执行。
对上面这句话的理解: Node采用的是异步 I/O模型,同步API在主线程中执行,异步API在底层的 C++ 维护的线程中执行,异步API的回调函数其本身是一个同步代码,因此是在主线程中调用执行,在js应用运行时,众多的异步API的回调函数什么时候才能回到主线程中被调用呢?这就是事件循环机制要做的事,管理异步API的回调函数什么时候回到主线程中执行。
2.2 为什么这种机制叫做事件循环?
因为Node是事件驱动的。事件驱动就是当什么时候做什么事,做的事情就定义在回调函数中,可以将异步API的回调函数理解为事件处理函数,所以管理异步API回调函数什么时候回到主线程中调用的机制叫做事件循环机制。
2.3 node事件环与浏览器事件环有什么区别?
在node10版本以后,可以理解成为和浏览器事件环的执行顺序一致,只是node的事件队列多了一些队列,有一些内部的队列无法控制,比如padding callbacks队列,idle,prepare队列,在node10版本之前,都是统一清空某个队列后才会去执行清空微任务队列,10之后是一个个取出队列里面的回调,执行完后清空一遍微任务,再取出一个执行,再清空微任务,以此类推。
3. Node Event Loop 的六个阶
事件循环本身是一个循环体,在循环体中有六个阶段,在每个阶段中都有一个事件队列,不同的事件队列存储了不同类型的异步API的回调函数。也就是说,事件循环在每一次的循环中都有六个阶段的事情要做(前提是要有)
如下图的libuv线程池就是定义了六个阶段的执行顺序,当然,顺序也得分情况
3.1 阶段概述:
- timers阶段:用于存储定时器的回调函数(setTimeout、setIntever)
- Padding Callbacks:执行与操纵系统相关的回调函数,比如启动服务器端应用时监听端口操作的回调函数就在这里调用;
- Idle,prepare:系统内部使用,开发者无法干预
- IO poll:存储I/O操作的回调函数队列,比如文件读写操作的回调函数; 如果该事件队列中有回调函数, 则会每次会取出一个到主线程中执行,直到该队列清空, 否则事件循环将在此阶段停留一段时间以等待新的回调函数进入,这个等待取决于一下两个条件: ①setImmediate队列(check阶段)中存在要执行的回调函数。 ②timers队列中存在要执行的回调函数,在这种情况下,事件循环将移至check阶段,然后移至closing callbacks阶段,并最终以timers阶段进入下一次循环。
- Ckeck:存储
setImmediate
API的回调函数; - Closing Callbacks:执行与关闭时间相关的回调,例如关闭数据库连接的回调,关闭socket的回调等。
循环体会不断运行以检测是否存在没有调用的回调函数。事件循环机制会按照先进先出的方式执行他们直到队列为空。
3.2 要点
- node与浏览器不同,node中的移步任务存在多个任务队列,但是也是每次只会取出一个满足条件的任务到栈中执行,执行完后,清空微任务再继续取出下一个执行
- 微任务队列中,
process.nextTick
是微任务,也是微任务中优先级最高的微任务,会在所有微任务之前执行。 setImmediate
代表立即执行的定时器,下面会做对比。- 若主线程被阻塞,例如while(true){},那么libuv线程里面的异步任务是没有被阻塞的,他们会等到时间到或者满足条件后,加入到自己的事件队列,等待被取出调用执行
- node中异步任务存在多个事件队列,如上图,(libuv线程池默认有4个线程)
- node内部会有一个最大执行数量限制,当队列里面的任务超过最大执行数量,那么内部会自己控制将超出的部分放到下一个事件循环里面执行,否则会一直卡在某个阶段,这是内部控制的,开发者无法干预。 也就是padding callbacks,和idle,prepare队列我们是无法控制的。
4. setImmediate() 对比 setTimeout()
setImmediate() 和 setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。
setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。 setTimeout() 在最小阈值(ms 单位)过后运行脚本。
例如: 以下运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:
js
// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
输出结果不确定谁先执行:
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用:
js
// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
输出结果,总是immediate先输出:
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
使用 setImmediate() 相对于setTimeout() 的主要优势是,如果setImmediate()是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关
5. 参考资料
Nodejs官方文档:Node.js 事件循环,定时器和 process.nextTick()