Skip to content

Node.js: использование Event Loop в качестве обычного цикла

16/03/2010

Сегодня мы попробуем использовать Event Loop в качестве обычного цикла. К примеру, мы хотим сделать на NodeJS онлайн-игру, и нам нужен game loop, выполняющий определённые действия как можно чаще, и при этом не мешающий остальным обработчикам. Какие у нас есть варианты?

Вообще то event loop практически не доступен изнутри Node, он прозрачен для пользователя. Но для нашей задачи желательно выполнять определённое действие каждую итерацию этого цикла. При этом полностью блокировать цикл нельзя — Node не сможет общаться с внешним миром. Т.е. просто написать while(true) недостаточно.

Начал свои опыты я с простого подхода — цикл по таймеру. Есть Event Emitter, у него есть событие beat. В конце этого события устанавливается таймер, вызывающий то же событие ещё раз. Это бесконечный цикл, при этом не блокирующий выполнение других обработчиков в Node. EventEmitter + setTimeout взяты вместо setInterval для большего контроля — на Emitter можно удобно навешивать дополнительные обработчики, а с setInterval нам придётся вызывать их явно.

var sys = require("sys"),
   http = require("http");
var events = require("events");

(function(){
    var pulse_num = 0;
    var heartbeat = new events.EventEmitter();

    heartbeat.addListener('beat', function() {

        pulse_num += 1;

        setTimeout(function(){
            heartbeat.emit('beat');
        }, 1000);

    });

    http.createServer(function(request, response) {
        response.sendHeader(200, {"Content-Type": "text/plain"});
        response.write("Hello, World. Pulse time is " +  pulse_num + "\n");
        response.close();
    }).listen(8000);

    sys.puts("Pulse server running at http://192.168.175.128:8000/");

    heartbeat.emit('beat');
    sys.puts("Pulse beat started");
})();

Но одно событие в секунду — не слишком удобно. Попробуем избавиться от таймаута.

Прежде чем написать код, я хочу вкратце объяснить что именно я ищу. Если EventEmitter.emit() не запускает обработчик сразу же, а отправляет его в пайп, мы получим действительно бесконечный цикл, работающий в ритме внутреннего Event Loop. Если же обработчик выполняется сразу, мы просто намертво заблокируем таким трюком Event loop (и, возможно, вылетим по переполнению стека), и нам придётся довольствоваться интервалами/таймерами. Пробуем:

var sys = require("sys"),
   http = require("http");
var events = require("events");

(function(){
    var pulse_num = 0;
    var heartbeat = new events.EventEmitter();

    heartbeat.addListener('beat', function() {

        pulse_num += 1;

        heartbeat.emit('beat');
    });

    http.createServer(function(request, response) {
        response.sendHeader(200, {"Content-Type": "text/plain"});
        response.write("Hello, World. Pulse time is " +  pulse_num + "\n");
        response.close();
    }).listen(8000);

    sys.puts("Pulse server running at http://192.168.175.128:8000/");

    heartbeat.emit('beat');
    sys.puts("Pulse beat started");
})();

И сразу — красивый Segmentation Fault. Добавив консольный счётчик внутрь тела цикла, мы увидим что падение происходит не сразу, а в районе ~500 вызова. Действительно, похоже на переполнение.

Окей, теперь попробуем явно указать, что мы хотим выполнить код в следующей итерации Event loop. Для этого существует метод nextTick, принимающий функцию в качестве аргумента. Пробуем:

var sys = require("sys"),
   http = require("http");
var events = require("events");

(function(){
    var pulse_num = 0;
    var heartbeat = new events.EventEmitter();

    heartbeat.addListener('beat', function() {

        pulse_num += 1;

        process.nextTick(function(){
            heartbeat.emit('beat');
        }
    });

    http.createServer(function(request, response) {
        response.sendHeader(200, {"Content-Type": "text/plain"});
        response.write("Hello, World. Pulse time is " +  pulse_num + "\n");
        response.close();
    }).listen(8000);

    sys.puts("Pulse server running at http://192.168.175.128:8000/");

    heartbeat.emit('beat');
    sys.puts("Pulse beat started");
})();

После неслабых тормозов процесс удаётся прибить. Работает, но процессор нагружает по полной. В общем-то понятно, ЕМНИП обычно event loop крутится на чистом C, не используя контекст V8. А тут мы со своими обработчиками. Ради интереса замерял: миллион итераций на моей машине (внутри виртуалки) занимают 13.6 секунд. Это примерно 73000 итераций в секунду.

В общем, если кто-то задумает, как я, использовать Event Loop в качестве основного цикла игрового движка, я советую делать это на интервальном таймере (или setTimeout с EventEmitter‘ом, как в моём первом примере). Там, правда, есть другие грабли и неожиданности.

Предположим, мы выполняем итерацию нашего цикла раз в 40 миллисекунд, 25 раз в секунду. Предположим, из-за какого то особо умного гоблина итерация #14400 выполняется аж целых 60 миллисекунд. Что произойдет при этом с итерацией #14441? Правильный ответ — она выполнится через 60 миллисекунд после начала #14400 (при условии что в очереди не стоят другие обработчики). Если же #14400 выполняется 90 мс (накрывая два последующих срабатывания таймера), произойдёт ещё более интересная вещь: #14401 выполнится после окончания #14400, а #14402 не будет выполнен вообще — т.к. если одно срабатывание setInterval уже ждёт обработки, новые события от того же таймера просто отбрасываются. Соответственно, если игра отсчитывает время по количеству итераций основного цикла (или предполагает что на n итераций одного таймера обязательно приходится m итераций второго), разработчика могут ждать разнообразные сюрпризы.

P.S. Кстати, товарищ creationix сделал замечательный сайт — руководство по API Node. Я буду его использовать для ссылок на объяснение что делает тот или иной метод.

P.S.S.: немного поправил и расширил абзац с перекрывающимися интервальными таймерами.

10 комментариев
  1. Anton permalink

    А аналогичные действия в EventMachine (ruby) или Twisted (python) по производительности лучше или хуже не подскажете?

  2. qwe permalink

    а чем process.nextTick не устроил?

    • В больших количествах он адски тормозит, даже если мы ничего в нём не считаем.

      • qwe permalink

        то бишь, если нам надо «выполнять итерацию нашего цикла раз в 40 миллисекунд», то даже ставить таймер на 1ms и смотреть сколько времени прошло с прошлой итерации — выгоднее?

        • В таком случае проще использовать SetInterval. Но вообще — да, должно быть выгоднее.

  3. Alex permalink

    github.com/AlexGarpix/timeAction

Trackbacks & Pingbacks

  1. Node.js: использование Event Loop в качестве обычного цикла « nodeJS
  2. Атака на другой мир или серверный JavaScript | Alpha-Beta-Release Blog

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: