Node.js:setInterval()跳过调用

11
对于即将要使用node.js的项目,我需要在定期时间内执行各种日常维护任务。具体来说,有些任务每毫秒执行一次,有些任务每20毫秒执行一次(每秒50次),还有些任务每秒执行一次。所以我考虑使用setInterval(),但结果很有趣:许多函数调用被跳过了。
我使用的基准测试如下:
var counter = 0;
var seconds = 0;
var short = 1;
setInterval(function() {
        counter ++;
    }, short);
setInterval(function() {
        seconds ++;
        log('Seconds: ' + seconds + ', counter: ' +
             counter + ', missed ' +
             (seconds * 1000 / short - counter));
    }, 1000);

有一个长为1秒的计时器和一个可以使用变量short调整的短计时器,此处为1毫秒。每秒钟我们打印出短周期中预期的滴答数与实际更新短计时器次数之间的差异。

当短计时器为1毫秒时,它的行为如下:

2012-09-14T23:03:32.780Z Seconds: 1, counter: 869, missed 131
2012-09-14T23:03:33.780Z Seconds: 2, counter: 1803, missed 197
2012-09-14T23:03:34.781Z Seconds: 3, counter: 2736, missed 264
...
2012-09-14T23:03:41.783Z Seconds: 10, counter: 9267, missed 733

许多函数调用被跳过了。这里是10毫秒的情况:

2012-09-14T23:01:56.363Z Seconds: 1, counter: 93, missed 7
2012-09-14T23:01:57.363Z Seconds: 2, counter: 192, missed 8
2012-09-14T23:01:58.364Z Seconds: 3, counter: 291, missed 9
...
2012-09-14T23:02:05.364Z Seconds: 10, counter: 986, missed 14

更好了,但每秒大约会跳过一个函数调用。而对于20毫秒:
2012-09-14T23:07:18.713Z Seconds: 1, counter: 46, missed 4
2012-09-14T23:07:19.713Z Seconds: 2, counter: 96, missed 4
2012-09-14T23:07:20.712Z Seconds: 3, counter: 146, missed 4
...
2012-09-14T23:07:27.714Z Seconds: 10, counter: 495, missed 5

最后持续100毫秒:

2012-09-14T23:04:25.804Z Seconds: 1, counter: 9, missed 1
2012-09-14T23:04:26.803Z Seconds: 2, counter: 19, missed 1
2012-09-14T23:04:27.804Z Seconds: 3, counter: 29, missed 1
...
2012-09-14T23:04:34.805Z Seconds: 10, counter: 99, missed 1

在这种情况下,它跳过了很少的调用(33秒后间隔增加到2,108秒后增加到3)。
数字有所变化,但在运行之间惊人地保持一致:连续三次运行第一个1毫秒基准测试,在10秒后产生了延迟9267、9259和9253。
我没有找到关于这个特定问题的参考资料。有这个经常引用的Ressig文章和许多相关的JavaScript问题,但大多数假设代码在浏览器中运行而不是在node.js中。

现在来到了令人恐惧的问题:这里发生了什么?开个玩笑;显然是函数调用被跳过了。但我看不出规律。我认为长周期可能会阻止短周期,但在1毫秒的情况下没有任何意义。短周期函数调用不会重叠,因为它们只更新一个变量,并且即使有1毫秒的短周期,node.js进程也接近5%的CPU。负载平均值很高,约为0.50。但我不知道为什么一千次调用会如此拖累我的系统,因为node.js可以完美地处理更多的客户端;这必须是setInterval()非常消耗CPU(或者我做错了什么)。

一个显而易见的解决方案是使用更长的计时器对函数调用进行分组,然后多次运行短周期函数调用以模拟较短的计时器。然后使用长周期作为“扫帚车”,在较低间隔中漏掉任何调用。例如:设置20毫秒和1000毫秒setInterval()调用。对于1毫秒的调用:在20毫秒回调中调用它们20次。对于1000毫秒的调用:检查20毫秒函数调用了多少次(例如47),执行任何剩余的调用(例如3)。但这个方案可能会有点复杂,因为调用可能会以有趣的方式重叠;尽管它看起来可能不规则。
真正的问题是:可以更好地完成吗,无论是使用setInterval()还是node.js内的其他计时器?提前感谢。
4个回答

12

如何实现计时器功能?使用哪种分辨率的计时器?需要使用一个库吗? - alexfernandez
在谷歌上有许多高分辨率计时器脚本。http://www.sitepoint.com/creating-accurate-timers-in-javascript/ - zer02
实际上,它运行得足够好!至少对于20毫秒计时器来说是这样,但令我惊讶的是,它也可以在1毫秒计时器中工作。如果您愿意更新您的答案并包含链接,我会接受它。1错过156,2错过156,3错过157...10错过156等等。对于1毫秒计数器,它似乎会漂移,尽管缓慢。我想我必须小心堆栈深度。 - alexfernandez
不需要处理递归,因为setTimeout()不使用递归! - alexfernandez

8

请查看此文档:http://nodejs.org/api/timers.html#timers_settimeout_callback_delay_arg

需要注意的是,您的回调函数可能不会在准确的延迟毫秒数后被调用 - Node.js不能保证回调将在何时触发,也不能保证触发顺序。回调将尽可能接近指定的时间调用。

这是因为应用程序代码阻塞了事件循环。所有计时器和I/O事件只能在nextTick上处理。

您可以使用以下代码查看此行为:

setInterval(function() {
    console.log(Date.now());
    for (var i = 0; i < 100000000; i++) {
    }
}, 1);

尝试更改迭代次数并查看结果。

理想情况下,如果应用程序的滴答时间小于1毫秒,则定时器将被精确触发。但在实际应用中,这是不可行的。


我已经阅读了那个参考文献,但它并没有解释原因。在我的基准测试中,没有应用程序代码阻塞事件循环。关于nextTick的参考是有趣的,谢谢。然而,它只是把问题推迟了一层:process.nextTick()会多久触发一次?为什么,它能被改变吗? - alexfernandez
不,我并不是说process.nextTick()是解决方案。我想说的是,没有办法处理定时器和I/O事件比一个事件循环迭代的执行时间更频繁。 - Vadim Baryshev
明白了。但在没有负载的情况下,一个事件循环迭代需要多长时间? - alexfernandez
它取决于阻塞代码的数量和CPU速度。在我的Core i5示例中,使用setInterval而不循环不能提供1毫秒的精度。 - Vadim Baryshev

2
答案恰好是Vadim和zer02给出的结合体,所以我在这里留下了一篇写作。正如Vadim所说,系统无法处理过于频繁的更新,并且向系统添加一些负载并不会有所帮助。或者更确切地说,运行时无法处理;如果需要,系统应该能够每毫秒触发回调,但由于某种未经解释的原因,它通常不想这样做。
解决方案是使用准确计时器,正如zer02所评论的那样。不要被名称误导;使用的机制是相同的setTimeout(),但延迟根据剩余时间调整,直到定时器应该触发。因此,如果时间已经结束,则“准确计时器”将调用setTimeout(callback, 0),该函数立即运行。令人惊讶的是,系统负载比setInterval()低:在我的非常不科学的样本中,约为CPU的2%,而不是5%。
这个简单的函数可能会派上用场:
/**
 * A high resolution timer.
 */
function timer(delay, callback)
{
    // self-reference
    var self = this;

    // attributes
    var counter = 0;
    self.running = true;
    var start = new Date().getTime();

    /**
     * Delayed running of the callback.
     */
    function delayed()
    {
        callback(delay);
        counter ++;
        var diff = (new Date().getTime() - start) - counter * delay;
        if (!self.running) return;
        setTimeout(delayed, delay - diff);
    }

    // start timer
    delayed();
    setTimeout(delayed, delay);
}

使用方法是,只需调用new timer(delay, callback);。(是的,我颠倒了参数顺序,因为先有回调函数非常令人讨厌。)要停止它,设置timer.running = false
最后需要注意的是:setTimeout(callback, delay)不会像我担心的那样使用递归(即等待一段时间,然后调用回调函数),而只是将回调函数放入一个队列中,在全局上下文中轮到它时由运行时调用。

可能是一个愚蠢的问题,但是一旦开始了,你怎么停止它呢?我可以让它开始,但是无法停止它 :) - Ben Clarke
1
@BenClarke,我添加了一些代码来阻止它,使用timer.running = false - alexfernandez

0

我禁用了调试器,然后再次尝试。这次运行得很好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接