Nodejs 异步之 Timer & Tick; 篇

发布于 1周前 作者 caililin 来自 nodejs/Nestjs

Nodejs 异步之 Timer & Tick; 篇

<div><strong>Timer:</strong></div> <br/><div><strong></strong> <br/>在前端开发中,我们 经常会使用setTimeout 函数组,这组函数其实不属于语言标准,他们只是extentsion ,在浏览器中,他们属于 BOM(浏览器对象扩展),即它的确切定义为:window.setTimeout ,和window.alert , window.open 等函数处于同一层次。<!–more–></div> <br/><div><a href=“http://code.google.com/p/doctype/wiki/WindowSetTimeoutMethod”>http://code.google.com/p/doctype/wiki/WindowSetTimeoutMethod</a></div> <br/><div> <br/> <br/>你可以在浏览器的控制台监测window 对象 ,或查看chromium 的实现 : <br/><a href=“http://codesearch.google.com/#OAMlx_jo-ck/src/third_party/WebKit/Source/WebCore/page/DOMWindow.h”>http://codesearch.google.com/#OAMlx_jo-ck/src/third_party/WebKit/Source/WebCore/page/DOMWindow.h</a> <br/> <br/>在nodejs ,当然也要实现自己的Timer ,在nodejs 的src 目录下,timer.h/cc 是对 libev 中的 ev_timer 进行了一层浅包装,在src/node.js 文件中,把这组函数放置于全局范围中(相当于浏览器中window) <br/><pre>startup.globalTimeouts = function() { <br/> global.setTimeout = function() { <br/> var t = NativeModule.require(‘timers’); <br/> return t.setTimeout.apply(this, arguments); <br/> }; <br/> global.setInterval = function() { <br/> var t = NativeModule.require(‘timers’); <br/> return t.setInterval.apply(this, arguments); <br/> }; <br/>…</pre> <br/>通过我们对libev的分析,我们已经知道,在libev中队timer的 添加/删除/更新 操作都是 O(lg(n)) , <br/>(libev 分析 <a href=“http://cnodejs.org/blog/?p=2489”>http://cnodejs.org/blog/?p=2489</a>) <br/> <br/>但其实在我们的日常开发中,以网络套接字为例,我们往往会对其设置timeout,即如果一段时间(120s 或 60s)此套接字没有活动事件发生,我们就关闭它(这种技巧非常重要,否则由于一些内部或外部的原因,很容易出现描述符占用过多的现象),注意到,我们的超时时间是一个统一的值,这时候,我们可以采用如下算法:(lib/linklist.js 真实实现) <br/> <br/><a href=“http://static.data.taobaocdn.com/up/nodeclub/2011/09/link.png”><img class=“alignnone size-full wp-image-2674” src=“http://static.data.taobaocdn.com/up/nodeclub/2011/09/link.png” alt="" width=“346” height=“365” /></a> <br/> <br/>当我们在不同的时刻多次 调用 setTimeout(fn ,1000) 时,我们把所有的这些事件仅由一个timer 来管理, 这些事件被放到一个双向链表中,其顺序自然而然的就是按时间来排序的,当某一时刻,timer 触发时,会依次的从prev链表头中取出节点,进行检查, 如果超时则触发… <br/>(参见lib/timer_legacy.js 由于libev无法在windows上 很好的工作,所以nodejs项目组又对libev,libeio 作了一层封装 ,称为libuv, 使用了传统的方法文件以_legacy 结尾,使用了libuv 的以_uv 结尾,如net_legacy.js ,net_uv.js ,timers_legacy.js  ,timers_uv.js 等,) <br/> <br/>当我们需要更改某异步事件时,比如,我们添加超时控制的socket上发生了read/ write 事件,这时候,我们需要重新reset 计时,我们仅需要把这个事件从链表中转移到链表尾部即可 <br/>(参见 lib/net_legacy.js) <br/> <br/>删除时的情况与此类似,所以在此场景下,仅耗费一个ev_timer ,而我们的所有异步事件的添加/删除/更新 都是 O(1) 复杂度. <br/> <br/>注意,在lib/timer*.js  中,当超时值<= 0 时是不做优化的, <br/><pre>exports.exports.setTimeout = function(callback, after) { <br/> if (after <= 0) { <br/> // Use the slow case for after == 0 <br/> timer = new Timer(); <br/> timer.ontimeout = callback; <br/> else{ <br/> … <br/> }</pre> <br/>同时setInterval 函数也是没做优化的. <br/> <br/><strong>nextTick :</strong> <br/> <br/>nextTick是个很有意思的东西,它其实是一个prepare watcher 来实现的,在libev中的event loop 的每次迭代,在nodejs 中就叫做 “Tick” ,而在libev 中,支持prepare watcher ,它在每次迭代开始时触发  ,在src/node.cc 中 我们看到类似代码(v4.9为例): <br/><pre>ev_prepare_init(&node::prepare_tick_watcher, node::PrepareTick); <br/>ev_prepare_start(EV_DEFAULT_UC_ &node::prepare_tick_watcher); <br/>ev_unref(EV_DEFAULT_UC);</pre> <br/>在函数 PrepareTick 中 会调用一个回调函数,此函数 _tickCallback 在 src/node.js 中: <br/><pre>startup.processNextTick =function() { <br/> var nextTickQueue = []; <br/> process._tickCallback = function() { <br/> … <br/> } <br/> process.nextTick = function(callback) { <br/> nextTickQueue.push(callback); <br/> process._needTickCallback(); <br/> }; <br/>};</pre> <br/>当我们调用API  process.nextTick 时,其实就是向nextTickQueue  这个闭包队列中添加一个函数,当下次事件循环开始时,会自动触发prepare_tick_watcher ,调用我们设定的函数。 <br/>注意,上面有个问题,那就是如果没有机会进行下次事件循环,比如,此时没有实质性的watcher可供监测了,这时事件循环就会退出 ,为了避免着这种情况, nextTick  函数中会调用一次 process._needTickCallback() ,这个函数会使tick_spinner (一个idle watcher)处于活跃状态 , 来防止事件循环的退出。 <br/> <br/>由nextTick 原理我们可知,当我们添加一个待处理事件时,其复杂度为O(1) ,但如果你用setTimeout(fn ,0) 的话,如我们在libev分析中所讲,其一进一出均为 O(lg(n)) , 所以nodejs官方文档讲 : <br/><blockquote>On the next loop around the event loop call this callback. This is not a simple alias to setTimeout(fn, 0), it’s much more efficient.</blockquote> <br/>nextTick 确实高效的多,诚不我欺也! <br/> <br/><strong>小节: <br/></strong>通过以上分析 ,我们至少得出2条粗浅的结论: <br/>#1  如果可能的话,调用setTimeout时,尽量使用相同的超时值 <br/>#2  尽量用process.nextTick 来代替 setTimeout(fn ,0) <br/> <br/></div>


9 回复

Nodejs 异步之 Timer & Tick; 篇

在前端开发中,我们经常会使用 setTimeout 函数组,这组函数其实不属于语言标准,它们只是扩展。在浏览器中,它们属于 BOM(浏览器对象模型)的一部分,即它们的确切定义为 window.setTimeout,和 window.alert, window.open 等函数处于同一层次。

Timer

在 Node.js 中,也有类似的定时器功能,它们是在 libev 库的基础上进行封装的。在 Node.js 的 src 目录下,timer.htimer.cc 文件是对 libev 中的 ev_timer 进行了一层浅包装。在 src/node.js 文件中,这些定时器函数被放置于全局范围内(相当于浏览器中的 window)。

startup.globalTimeouts = function() {
  global.setTimeout = function() {
    var t = NativeModule.require('timers');
    return t.setTimeout.apply(this, arguments);
  };

  global.setInterval = function() {
    var t = NativeModule.require('timers');
    return t.setInterval.apply(this, arguments);
  };
}

在实际应用中,例如在网络套接字中设置超时控制时,我们通常会为每个套接字设置一个统一的超时时间(例如 120 秒或 60 秒)。如果在这段时间内没有活动事件发生,我们将关闭该套接字。在这种情况下,我们可以通过将所有这些事件放入一个双向链表来管理,从而使得超时检查变得高效。

// 示例代码:使用 setTimeout 设置超时控制
const net = require('net');

const server = net.createServer(socket => {
  socket.setTimeout(120000); // 设置超时时间为 120 秒

  socket.on('timeout', () => {
    console.log('Socket has timed out');
    socket.destroy(); // 关闭超时的套接字
  });

  socket.on('data', data => {
    console.log(`Received data: ${data}`);
    socket.setTimeout(0); // 重置超时计时
  });
});

server.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

nextTick

nextTick 是一个非常有趣的功能,它实际上是由一个 prepare watcher 实现的。在 Node.js 的事件循环中,每次迭代被称为“Tick”,而在 libev 中,支持 prepare watcher,它在每次迭代开始时触发。在 src/node.cc 文件中,我们可以看到类似以下的代码:

ev_prepare_init(&node::prepare_tick_watcher, node::PrepareTick);
ev_prepare_start(EV_DEFAULT_UC_ &node::prepare_tick_watcher);
ev_unref(EV_DEFAULT_UC);

PrepareTick 函数中,会调用一个回调函数 _tickCallback,该函数在 src/node.js 文件中定义:

startup.processNextTick = function() {
  var nextTickQueue = [];

  process._tickCallback = function() {
    while (nextTickQueue.length) {
      const callback = nextTickQueue.shift();
      callback();
    }
  };

  process.nextTick = function(callback) {
    nextTickQueue.push(callback);
    process._needTickCallback();
  };
};

当我们调用 process.nextTick 时,实际上是将一个函数添加到 nextTickQueue 队列中,并在下次事件循环开始时自动触发。这样可以确保即使没有其他活动的 watcher,事件循环也不会退出。

小结

通过以上分析,我们可以得出以下两个结论:

  1. 如果可能的话,尽量使用相同的超时值来调用 setTimeout
  2. 尽量使用 process.nextTick 来代替 setTimeout(fn, 0),因为它更高效。

希望这些内容能够帮助你更好地理解 Node.js 中的定时器和 nextTick 机制。


最后小结收益良多啊,希望爱多兄能有更多深入浅出的文章~ <br/>继续关注中~

timer复用。。。解除疑惑。。。以后得多深入源码。。

问一个问题,我在一个框架中看到类似这样的一份实现:

function classA(fct){this.fct = fct;};
classA.prototype.run()=function(){
var self = this;
setTimeout(function(){this.fct.call(self,self)},1);
return this;}
var instance = new ClassA(function(){console.log(1)});
instance.run();

其实这个run方法的调用是很频繁的,框架中的很多语句都会去创建classA对象执行run方法. 我想知道这里的setTimeout调用的目的是为了?

好厉害!学习了

好坟。。。

最新的nodejs不要用nextTick。用setImmediately

Nodejs 异步之 Timer & Tick; 篇

Timer

在Node.js中,setTimeoutsetInterval 是用来处理定时任务的核心函数。它们并不是JavaScript语言标准的一部分,而是Node.js提供的一种扩展功能。与浏览器环境不同,Node.js中的这些函数是由libuv库实现的。

// 示例代码
console.log("Start");

setTimeout(() => {
  console.log("Timer 1");
}, 1000);

setTimeout(() => {
  console.log("Timer 2");
}, 2000);

console.log("End");

上述代码展示了如何使用 setTimeout。虽然这两个定时器的时间不同,但是Node.js会将它们统一管理,并在合适的时间执行。

另外,对于网络套接字的超时控制,可以通过设置统一的超时时间来优化性能。例如:

const net = require('net');

const server = net.createServer((socket) => {
  socket.setTimeout(120 * 1000); // 设置超时时间为120秒

  socket.on('timeout', () => {
    console.log('Socket has timed out');
    socket.end(); // 超时后关闭套接字
  });

  socket.on('data', (data) => {
    console.log(`Received data: ${data.toString()}`);
    socket.setTimeout(0); // 重置超时时间
  });
});

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});

nextTick

process.nextTick 是一个高效的方式来处理微任务。它在当前事件循环结束时立即执行,而不是等待下一个事件循环。这使得它比 setTimeout(fn, 0) 更加高效。

console.log("Start");

process.nextTick(() => {
  console.log("nextTick 1");
});

process.nextTick(() => {
  console.log("nextTick 2");
});

console.log("End");

上述代码展示了如何使用 process.nextTick。尽管它看起来与 setTimeout 类似,但它会在当前事件循环结束后立即执行,因此它的执行优先级高于其他I/O操作和定时器。

小结

  • 使用相同的超时值:如果可能,尽量使用相同的超时值来优化定时任务的管理。
  • 使用 process.nextTick:相比于 setTimeout(fn, 0)process.nextTick 更高效,应该优先使用。
回到顶部