【9.1】node 原理 - 异步 I/O - setTimeout 原理,process.nextTick 和 setImmediate 对比

828 阅读4分钟

这个系列第一篇文章从用户体验、资源分配,这两个方面介绍了我们为什么需要异步 I/O。这一篇文章主要讲异步 I/O 在操作系统层面的支持状况。第二篇文章介绍了异步 I/O 在操作系统层面的支持情况,异步和非阻塞着两个概念的区别。第三篇文章介绍了 nodejs 异步 I/O 的实现方式。

我们先来回顾一下异步 I/O 的实现方式:

  1. JavaScript 层进行 api 调用,比如 fs.open()
  2. nodejs 内部 JavaScript 层核心模块,调用 c++ 内建模块,c++ 内建模块依赖 libuv 调用底层操作系统方法
  3. 调用方法时会封装一个请求对象,把 callback 放入对象的属性中
  4. 将请求对象加入 I/O 线程池,等待执行
  5. 线程空闲时,执行对应的 I/O 操作
  6. 执行完毕后,把结果放到 req->result 上,调用方法通知释放线程,同时修改状态
  7. nodejs 事件循环,在单次 Tick 执行时,会通过方法检查 I/O 线程池中任务执行的状态,放入 I/O 观察者的队列中
  8. 后面 Tick 执行时,会向 I/O 观察者询问是否有事件要执行
  9. 有的话从 I/O 观察者中请求对象取出 result 和 callback 执行,至此回调函数执行完成了~

本次文章主要讲和 I/O 无关的异步 API,它们分别是 setTimeout()、setInterval()、 setImmediate()和 process.nextTick()

定时器

setTimeout 和 setInterval 和浏览器中的 API 是一致的,分别是单次和多次执行定时任务,他们的原理和异步 I/O 类似,但不会有 I/O 线程池参与。

  1. 调用 setTimeout
  2. 创建定时器对象,放入定时器观察者的红黑树中
  3. 事件循环中,每次 Tick 执行时,会从定时器观察者和红黑树中,迭代取出定时器对象,检查是否超过定时时间
  4. 如果超过就形成一个事件,它的回调函数立即执行

定时器的问题在于,他并非是精确的(在容忍范围内),尽管事件循环很快,但是如果某次循环占用的时间过多,那么下次循环时它可能超时很久了。比如一个任务在 10ms 后执行,但是在 9ms 后,有一个任务占用了 5ms 的 cpu 时间片,那么轮到定时器执行时,时间就过了 4ms 了。

image.png

process.nextTick

process.nextTick 只会将回调函数放入队列中,下一次 Tick 时取出执行,适合立即执行一个异步任务。

我们之前可能会使用 setTimeout(fn, 0),其实是不好的,setTimeout 因为使用到了红黑树,操作时间复杂度是 O(lg(n)),nextTick 是 O(1),nextTick 更加高效

setImmediate

setImmediate 和 nextTick 有类似之处,我们先来看下他们的执行顺序:

process.nextTick(function () { 
    console.log('nextTick延迟执行');
});
setImmediate(function () {
    console.log('setImmediate延迟执行'); 
});
console.log('正常执行');

nextTick 的执行是优先于 setImmediate 的,因为事件循环对于观察者的检查是有先后顺序的,nextTick 属于 idle 观察者,setImmediate 属于 check 观察者,在每一轮循环的检查中,观察者的优先级是:idle 观察者 > I/O 观察者 > check 观察者。

具体实现上,nextTick 将回调函数保存在数组中,每次循环会将数组中的回调函数全部执行,setImmediate 会将回调函数保存在链表中,每次执行链表中第一个回调函数。举例:

// 加入两个nextTick()的回调函数
process.nextTick(function () {
    console.log('nextTick延迟执行1'); 
});

process.nextTick(function () { 
    console.log('nextTick延迟执行2');
});  
// 加入两个setImmediate()的回调函数
setImmediate(function () {
    console.log('setImmediate延迟执行1'); // 进入下次循环
    process.nextTick(function () {
        console.log('强势插入'); 
    });
});
setImmediate(function () {
    console.log('setImmediate延迟执行2'); 
});

console.log('正常执行');

// 正常执行 
// nextTick延迟执行1 
// nextTick延迟执行2 
// setImmediate延迟执行1 
// 强势插入 
// setImmediate延迟执行2

正常执行

第一次 Tick:执行 nextTick 中的保存的数组中所有回调函数,执行 setImmediate 保存到链表中的第一个回调函数

第二次 Tick:执行 nextTick 中的保存的数组中所有回调函数,执行 setImmediate 保存到链表中的第二个回调函数

setImmediate 这样设计是为了让每次循环可以较快的结束,防止占用过多 CPU,阻塞后续的 I/O 调用

以上是setTimeout 原理,process.nextTick 和 setImmediate 对比,欢迎点赞和评论~