17. Уроки Node.js. Таймеры, Отличия от Браузера, ref и unref

timeout

Всем привет! Эта статья посвящена таймерам в Node.js. В ней мы постараемся в первую очередь рассказать о тех различиях, которые есть между таймерами в браузерах и в Node.js. Для этого откроем документацию, и увидим, что несколько методов здесь очень похожи:

Timers

  • setTimeout(callback, delay, [arg], […])
  • clearTimeout(timeoutId)
  • setInterval(callback, delay, [arg], […])
  • clearInterval(intervalId)

Работа практически одинакова, что в Node.js, что в браузерах. А дальше начинаются различия. Первое отличие мы посмотрим на примере такого вот сервера:

Как видим, сервер здесь очень простой, можно сказать, абстрактный http-сервер, который слушает порт 3000 и что-то там делает, неважно что, с запросами. В определенный момент, например, через 2,5 сек, мы решаем прекратить функционирование этого сервера. При вызове server.close();  сервер прекращает принимать новые соединения, но пока есть принятые и неоконченные запросы, они еще будут обрабатываться, и только тогда, когда все соединения будут обработаны и закрыты, тогда процесс прекратится. В данном случае я запускаю, и если никаких запросов нет, то через 2,5 сек процесс, как видим, завершился. Все пока понятно и предсказуемо.

А теперь предположим, что, по мере работы этого сервера, я хочу постоянно получать информацию об использовании памяти в console. Что ж нет ничего проще. Каждую секунду выводим специальный вызов procces.memoryUsage:

Запускаю получившийся сервер. Смотрим. Информация, информация… Сервер работает, работает… Но в чем же дело? Почему процесс не завершился, ведь прошло уже больше, чем 2,5 сек? Конечно же, во всем виноваты вот эти строки:

Как мы помним за таймеры, за события ввода-вывода отвечает библиотека libUV. Пока есть активный таймер, libUV не может завершить процесс. Что делать? Давайте рассмотрим несколько решений. Первое решение – это сделать callback в функции close, который сработает, когда сервер полностью обработает и закроет все соединения. В нем написать process.exit:

Давайте попробуем. Работает! С одной стороны, нормально, с другой стороны, как-то это слишком брутально, просто жесткое перебивание процесса. Давайте чуть-чуть мягче, будем очищать таймер:

Тоже все хорошо, но архитектурно и это решение далеко не самое лучшее. Давайте подумаем. Вот сервер:

Он может быть в одном файле, а этот SetInterval может быть совсем в другом модуле:

А может быть еще и третий модуль, который держит тоже свой сервер или осуществляет какие-то свои операции. Представим себе, что будет, если мы здесь:

сделаем “exit”.

В этом случае вообще весь процесс умрет, и даже те операции, которые сервера не касаются, если они есть. Плохо! Теперь просто остановится вывод сообщений, но другие операции продолжат выполняться. Это немного лучше, но все равно не так хорошо.

На самом деле, правильное решение  будет в использовании специализированных возможностей Node.js. А именно, использую специальный метод, который называется timer.unref:

Как видим, в отличие от браузерного JavaScript, здесь timer – это объект, и метод timer.unref указывает libUV, что этот таймер является второстепенным. То есть, его не следует учитывать при проверке внутренних watcher на завершение процесса. Запустим. Как только сервер закончит работу, как только не останется никаких других внутренних watcher, кроме таймера unref, процесс завершиться.

Есть еще метод  ref. Он является противоположным unref. То есть, если я сделал timer.unref, потом     передумал, то могу вызвать метод ref. В практике он используется очень редко.
Почему это решение лучше? Просто потому что здесь таймер указывает, что он неважен, что, по сути, нам и требуется. Никаких побочных эффектов это не несет.

Обращаем ваше внимание, что метод unref есть не только у таймеров. Он есть еще и у серверов или у сетевых сокетов. То есть, я могу сделать сетевое соединение, которое тоже не будет препятствовать завершению процесса, если оно почему-то неважно.

Далее в документации мы видим методы setImmediate(), setInterval(), которые тоже отличаются от браузерных. Для того, чтобы лучше это понять, рассмотрим следующий пример. У нас есть веб сервер, и там функции обработчика запроса понадобилось выполнить какую-то операцию асинхронно.

В браузере для этого обычно используется либо setTimeout 0, либо вызов setImmediate или его эмуляция различными hacks. Но, обращаю ваше внимание, в браузере немного по-другому работает событийный цикл, и setImmediate браузера немного не тот. Сейчас мы его обсуждать не будем. Посмотрим, что происходит в Node с setTimeout 0. Когда сработает этот код? Можем ли мы гарантировать, что он выполнится до того, как придет следующий запрос? Конечно же, нет. setTimeout выполнит его в ближайшее время, но когда – совершенно не понятно, может быть до следующего запроса или после. Однако, есть такие ситуации, когда мы должны четко знать, что некий асинхронный код выполнится до того, как в Node придет следующий запрос или любое другое событие ввода-вывода.  Например, потому что мы хотим повесить обработчик, скажем, у нас есть request, и мы хотим повесить на него в этом setTimeout обработчик на следующие данные:
Мы должны точно знать, что этот обработчик повесится до того, как эти следующие данные будут прочитаны. Для решения этой задачи в Node есть специальный вызов process.nextTick. Он, с одной стороны, сделает выполнение функции асинхронным, то есть она выполнится после выполнения текущего JavaScript, а с другой стороны он гарантирует, что выполнение произойдет до того, как придут следующие события ввода-вывода, таймера и т.д. Более того, если при обработке этой функции, process.nextTick, выяснится, что нужно что-то еще асинхронно запланировать, то вложенные рекурсивные вызовы process.nextTick тоже добавят выполнение функций сюда же. Таким образом, мы можем гарантировано повесить обработчики, они сработают до того, как придут какие-то еще данные.

Бывает и другая ситуация, когда мы хотим сделать функцию асинхронной, но при этом не тормозит событийный цикл. Частный пример – это когда у нас есть большая вычислительная задача, то чтобы JavaScript не блокировался здесь надолго, мы можем попробовать его разбить на части. Одну часть запустить тут же, а другую запустить так, чтобы она сработала на следующей итерации этого цикла, третья на следующей и т.д. Для реализации этого в Node.js есть вызов setImmediate. Он как раз и планирует выполнение функции так, чтобы она, с одной стороны, сработала как можно скорее, а с другой стороны, на следующей итерации цикла после обработки текущих событий.

Рассмотрим отличия между nextTick и setImmediate на конкретном примере:

Здесь мы используем модуль fs для того, чтобы открыть файл. Открытие файла здесь прост, как вариант операции ввода-вывода. Когда файл будет открыт, то сработает внутреннее событие libUV, которое вызовет эту функцию. Дальше мы через setImmediate и nextTick планирую вывод сообщений. Посмотрим, в каком порядке они выведутся. Как вы думаете, в каком? Запустим.

Итак, сначала, конечно же, вывелся nextTick, потому что он планируется по окончанию текущего JavaScript, но до любых событий ввода-вывода, то есть, до реального открытия файла. setImmediate сработала после ввода-вывода, потому что она так запланировала выполнение. А если б мы сюда добавили setTimeout 0, где был бы он?  А вот неизвестно.

Итак, мы рассмотрели, чем таймеры в  Node.js отличаются от браузерных.

  • Во-первых, это влияние на завершение процесса и методы ref/unref.
  • Во-вторых, это то, что есть различный setTimeout 0, есть process.nextTick и есть setImmediate. В большинстве ситуаций используется nextTick. Он гарантирует, что выполнение произойдет до новых событий, в частности, до новых операций ввода-вывода, до новых данных. Как правило, это наиболее безопасный вариант.
  • setImmediate планирует выполнение на следующую итерацию цикла, после обработки событий. Как правило, это нужно либо тогда, когда нам без разницы, обработаются какие-то события или нет, то есть, мы хотим что-то сделать асинхронно, и нам не хочется лишний раз тормозить событийный цикл; либо при разбитии сложной задачи на части, чтобы одну часть обработать сейчас, другую – на следующее итерации цикла, следующую  – потом. При этом получается, что задача, с одной стороны, постепенно делается, а с другой стороны, между ее частями могут проскакивать какие-то другие события, другие клиенты. И серьезной задержки в обслуживании не произойдет

Код урока вы можете найти в нашем репозитории

software-computer-code-1940x900_35196

Материалы статьи взяты из следующего скринкаста

We are looking forward to meeting you on our website soshace.com

About the author

Stay Informed

It's important to keep up
with industry - subscribe!

Stay Informed

Looks good!
Please enter the correct name.
Please enter the correct email.
Looks good!

Related articles

Уроки Express.js . Логгер, Конфигурация, Шаблонизация с EJS. Часть 2.

Favicon – это все connect Middleware, он смотрит, если url имеет вид favicon.ico, то он читает favicon и ...

3. Уроки Express.js. Шаблонизация с EJS: Layout, Block, Partials

В реальной жизни у нас обычно больше, чем один шаблон. Более того, если уж так ...

24.11.2016

Уроки Express.js. Основы и Middleware. Часть 2.

Всем привет! Давайте продолжим наш урок об основах Express и Middleware. Итог (добавим в ...

No comments yet

Sign in

Forgot password?

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy

Password recovery

You can also try to

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy