一直以来,我写的的大部分JS代码都是在浏览器环境下运行,因此也了解过浏览器的事件循环机制,知道有macrotask和microtask的区别。但最近写node时,发现node的事件循环机制和浏览器端有很大的不同,特此深入地学习了下。
单线程
在传统web服务中,大多都是使用多线程机制来解决并发的问题,原因是I/O事件会阻塞线程,而阻塞就意味着要等待。而node的设计是采用了单线程的机制,但它为什么还能承载高并发的请求呢?因为node的单线程仅针对主线程来说,即每个node进程只有一个主线程来执行程序代码,但node采用了事件驱动的机制,将耗时阻塞的I/O操作交给线程池中的某个线程去完成,主线程本身只负责不断地调度,并没有执行真正的I/O操作。也就是说node实现的是异步非阻塞式。
事件循环机制
node能实现高并发的诀窍就在于事件循环机制,这个事件循环机制和浏览器端的相似但也有很多不同。根据node的官方介绍,node每次事件循环机制都包含了6个阶段:
- timers阶段:这个阶段执行已经到期的timer(setTimeout、setInterval)回调
- I/O callbacks阶段:执行I/O(例如文件、网络)的回调
- idle, prepare 阶段:node内部使用
- poll阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
- check阶段:执行setImmediate回调
- close callbacks阶段:执行close事件回调,比如TCP断开连接
对于日常开发来说,我们比较关注的是timers、I/O callbacks、check阶段。node和浏览器相比一个明显的不同就是node在每个阶段结束后会去执行所有microtask任务。对于这个特点我们可以做个试验:
console.log('main');
setImmediate(function() {
console.log('setImmediate');
});
new Promise(function(resolve, reject) {
resolve();
}).then(function() {
console.log('promise.then');
});
代码的执行结果是:
- main
- promise.then
- setImemediate
setImmediate 和 process.nextTick
相对于浏览器环境,node环境下多出了setImmediate和process.nextTick这两种异步操作。setImmediate的回调函数是被放在check阶段执行,即相当于事件循环的最后阶段了。而process.nextTick会被当做一种microtask,前面提到每个阶段结束后都会执行所有microtask任务,所以process.nextTick有种类似于插队的作用,可以赶在下个阶段前执行,但它和promise.then哪个先执行呢?通过一段代码来实验:
console.log('main');
process.nextTick(function() {
console.log('nextTick')
})
new Promise(function(resolve, reject) {
resolve();
}).then(function() {
console.log('promise.then');
});
代码的执行结果是:
- main
- nextTick
- promise.then
事实证明,process.nextTick的优先级会比promise.then高。
process.nextTick的饥饿陷阱
process.nextTick的优势在于它能够插入到每个阶段之后,在当前阶段执行完毕后就能立马执行。然而它的这个优点也导致了如果调用不当就容易陷入饥饿陷阱。具体就是当递归地调用process.nextTick的时候,事件循环一直无法进入到下一个阶段,导致了后面阶段的事件一直无法被执行,产生饥饿问题。
看一个例子就很容易明白
let i = 0;
setImmediate(function() {
console.log('setImmediate');
});
function callback() {
console.log('nextTick' i );
if (i < 1000) {
process.nextTick(callback);
}
}
callback();
执行的结果是 nextTick0 nextTick1 nextTick2 ... nextTick999 setImmediate
setImmediate的回调会一直等待到process.nextTick任务都完成后才能被执行。
小结
1.node的事件循环机制和浏览器的有所不同,多出了setImmediate 和 process.nextTick这两种异步方式。由于process.nextTick会导致I/O饥饿,所以官方也推荐使用setImmediate。 2.node虽然是单线程的设计,但它也能实现高并发。原因在于它的主线程事件循环机制和底层线程池的实现。 3.这种机制决定了node比较适合I/O密集型应用,而不适合CPU密集型应用。
来源:https://blog.csdn.net/yaly200/article/details/101264974