node.js: setInterval() пропускает вызовы

Для предстоящего проекта с 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);

Есть длинный таймер на одну секунду и короткий, который можно настроить с помощью переменной 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

В этом случае он пропускает очень мало вызовов (разрыв увеличился до 2 через 33 секунды и до 3 через 108 секунд).

Цифры различаются, но на удивление совпадают между запусками: запуск первого теста 1 мс три раза дал задержку после 10 секунд 9267, 9259 и 9253.

Я не нашел ссылок на эту конкретную проблему. Есть этот часто цитируемый пост Ressig и множество вопросов по JavaScript, но большинство предполагает, что код работает в браузере, а не в node.js.

А теперь самый неприятный вопрос: что здесь происходит? Просто шутка; очевидно, вызовы функций пропускаются. Но я не вижу закономерности. Я думал, что длинные циклы могут мешать коротким, но в случае с 1 мс это не имеет никакого смысла. Вызовы функций с коротким циклом не перекрываются, поскольку они просто обновляют переменную, а процесс node.js потребляет около 5% ЦП даже при коротком цикле в 1 мс. Однако средняя нагрузка высока, около 0,50. Я не знаю, почему тысяча вызовов так сильно нагружает мою систему, поскольку node.js обрабатывает намного больше клиентов; должно быть верно, что setInterval() интенсивно использует ЦП (или я делаю что-то не так).

Очевидным решением является группировка вызовов функций с использованием более длинных таймеров, а затем многократное выполнение вызовов функций с коротким циклом для имитации более короткого таймера. Затем используйте длинный цикл в качестве «вагона с метлой», который пропускает любые вызовы в более низких интервалах. Пример: настройте вызовы setInterval() на 20 мс и 1000 мс. Для вызовов 1 мс: вызовите их 20 раз в обратном вызове 20 мс. Для вызова 1000 мс: проверьте, сколько раз была вызвана функция 20 мс (например, 47), выполните оставшиеся вызовы (например, 3). Но эта схема будет немного сложной, поскольку вызовы могут интересным образом перекрываться; также это не будет регулярным, хотя это может выглядеть так.

Реальный вопрос: можно ли это сделать лучше с помощью setInterval() или других таймеров в node.js? Заранее спасибо.


person alexfernandez    schedule 14.09.2012    source источник


Ответы (4)


Функции SetInterval в javascript не точны. Попробуйте использовать таймер с высоким разрешением.Создание точных таймеров в javascript

person zer02    schedule 15.09.2012
comment
Как? Какое разрешение, таймеры, библиотека? - person alexfernandez; 15.09.2012
comment
В Google есть много сценариев таймера с высоким разрешением.sitepoint.com/creating-accurate -таймеры-в-javascript - person zer02; 15.09.2012
comment
На самом деле, это работает достаточно хорошо! По крайней мере, для таймера на 20 мс, но также (к моему удивлению) и для таймера на 1 мс. Если вы захотите обновить свой ответ и включить ссылку, я приму ее. 1 пропустил 156, 2 пропустил 156, 3 пропустил 157... 10 пропустил 156 и так далее. Для счетчика 1 мс кажется, что он дрейфует, хотя и медленно. Полагаю, мне придется быть осторожным с глубиной стека. - person alexfernandez; 15.09.2012
comment
Нет необходимости иметь дело с рекурсией, так как setTimeout() не использует рекурсию! - person alexfernandez; 15.09.2012

Посмотрите этот документ: http://nodejs.org/api/timers.html#timers_settimeout_callback_delay_arg

Важно отметить, что ваш обратный вызов, вероятно, не будет вызываться ровно через миллисекунды задержки — Node.js не дает никаких гарантий ни в отношении точного времени срабатывания обратного вызова, ни в порядке срабатывания элементов. Обратный вызов будет вызываться как как можно ближе к указанному времени.

Это происходит потому, что код приложения блокирует цикл обработки событий. Все таймеры и события ввода-вывода могут обрабатываться только на nextTick.

Вы можете увидеть это поведение с помощью этого кода:

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

Попробуйте изменить количество итераций и посмотрите результаты.

В идеале таймер сработает ровно в том случае, если тик приложений будет длиться менее одной мс. Но это нереально в реальном приложении.

person Vadim Baryshev    schedule 14.09.2012
comment
Я читал эту ссылку, но она не объясняет, почему. В моем тесте у меня нет кода приложения, блокирующего цикл обработки событий. Ссылка на nextTick интересна, спасибо. Однако это отодвигает проблему только на один уровень: как часто срабатывает process.nextTick()? Почему и можно ли это изменить? - person alexfernandez; 15.09.2012
comment
Нет, я не имел в виду process.nextTick() как решение. Я хотел сказать, что нет способа обрабатывать таймеры и события ввода-вывода чаще, чем время выполнения одной итерации цикла обработки событий. - person Vadim Baryshev; 15.09.2012
comment
Понял. Но как долго длится одна итерация цикла обработки событий при отсутствии нагрузки? - person alexfernandez; 15.09.2012
comment
Это зависит от количества блокирующего кода и скорости процессора. На моем примере с Core i5 с setInterval без цикла точность в 1 мс не выдается. - person Vadim Baryshev; 15.09.2012

Ответ оказался комбинацией ответов, данных Вадимом и zer02, так что я оставляю запись здесь. Как сказал Вадим, система не справляется со слишком частыми обновлениями, и дополнительная нагрузка на систему не поможет. Вернее, среда выполнения не справляется; система должна быть более чем способна запускать обратный вызов каждую миллисекунду, если это необходимо, но по какой-то необъяснимой причине часто она этого не хочет.

Решение заключается в использовании точных таймеров, как прокомментировал zer02. Не вводите в заблуждение название; используется тот же механизм setTimeout(), но задержка регулируется в зависимости от времени, оставшегося до срабатывания таймера. Итак, если время истекло, «точный таймер» вызовет setTimeout(callback, 0), который запускается немедленно. Загрузка системы, на удивление, меньше, чем с setInterval(): около 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) не использует рекурсию, как я опасался (например, подождите некоторое время, затем вызовите обратный вызов), он просто помещает обратный вызов в очередь, которая будет вызываться средой выполнения, когда придет время происходит в глобальном контексте.

person alexfernandez    schedule 17.09.2012
comment
Возможно, глупый вопрос, но как остановить это, когда оно уже началось? Я могу запустить его, но не могу остановить :) - person Ben Clarke; 23.12.2018
comment
@BenClarke Я добавил код, чтобы остановить это, используя timer.running = false. - person alexfernandez; 27.12.2018

Я отключил отладчик и попробовал еще раз. У меня все работало нормально.

person Sabiq Thottoly    schedule 21.12.2020