- PVSM.RU - https://www.pvsm.ru -
Юдель Пэн. Часовщик. 1924
«Компьютер — это конечный автомат. Потоковое программирование нужно тем, кто не умеет программировать конечные автоматы»
Алан Кокс, прим. Википедия
“Знай свой инструмент” — твердят все вокруг и все равно доверяют. Доверяют модулю, доверяют фреймворку, доверяют чужому примеру.
Излюбленный вопрос на собеседованиях по Node.js — это устройство Event Loop. И при всем том, очевидном факте, что прикладному разработчику эти знания будут полезны, мало кто пытается самостоятельно погрузиться в устройство событийного цикла. В основном, всех устраивает картинка сверху. Хоть это и похоже на пересказ фильма, который ты не смотрел, а о котором тебе рассказал друг.
Самое сложное, наверное для меня, это признавать свои ошибки и соглашаться с тем, что я чего-то не знаю. Об ошибках не любят говорить и писать. В основном, все любят писать и говорить о своих успехах и хороших историях, человек старается выстроить образ непобедимого героя.
А ведь как правило, ошибки допускаются именно по незнанию, именно из-за поверхностных суждений, из-за того, что кто-то потратил меньше времени, чем нужно, на изучение поставленного вопроса. Очевидность. Я знаю.
Ниже, я попробую описать мое понимание событийного цикла на примере исходного кода libuv (в тандеме с V8, это основа Node.js), а так же я примкну к когорте людей, которые твердят: “Надо знать свой инструмент”.
Кстати последнее, в современных реалиях, становится нелегким занятием. Один только npm насчитывает, на текущий момент, почти полмиллиона модулей, я уже и не говорю про армию репозиториев на github. Но так все устроено, чтобы оставаться на месте, нужно бежать, чтобы сдвинуться с места, нужно бежать в два раза быстрее.
Эта заметка в первую очередь напоминание мне, напоминание быть внимательнее. Читателю же, я рекомендую погрузиться в исходный код самостоятельно, сделать какие-то выводы, а потом вернуться к этому тексту.
Также, описанное ниже — это огромная аппроксимация того, что на самом деле происходит под капотом Node.js. Среди многих других, заметка базируется именно на исходном коде libuv. Я буду рассматривать кодовую базу библиотеки в части unix [1]. Код для win будет другим.
Событийно-ориентированное программирование [2] (СОП, Event-Driven Programming / EDP) — парадигма программирования, в которой выполнение программы определяется событиями.
Парадигма СОП активно применяется при разработке GUI, однако, применение ей нашлось и на стороне сервера. В 1999 году, обслуживая популярный в то время публичный FTP-сервер Simtel, его администратор Ден Кегель заметил, что узел на гигабитном канале по аппаратным показателям должен был бы справляться с нагрузкой в 10 000 соединений, но программное обеспечение этого не позволяло. Проблема была связана с большим количество программных потоков [3], каждый из которых создавался на отдельное соединение.
Идея событийного цикла, работающего в одном потоке, решала эту проблему. Подобные реализации есть не только в мире JavaScript (Node.js). К примеру, Asyncio и Twisted в Python, EventMachine и Celluloid в Ruby, Vert.x в Java. Еще один яркий представитель подобной реализации — прокси-сервер Nginx.
В основе СОП лежит Событийный Цикл (Event Loop) [4] — программная конструкция, которая занимается диспетчеризацией событий и сообщений в программе.
Цикл, в свою очередь, работает с асинхронным вводом/выводом [5], или неблокирующим вводом/выводом, что является формой обработки данных, который позволяет другим процессам продолжить выполнение до того, как передача будет завершена.
Функция обратного вызова (Callback) [6] — возможность передачи исполняемого кода в качестве одного из параметров другого кода. Подобная техника позволяет нам удобно работать с асинхронным вводом/выводом.
А теперь начнем с официального примера “Hello World!” сайта http://docs.libuv.org [7]:
Пример прост, резервируется необходимая оперативная память и инициализируется структура событийного цикла [8], далее он запускается в режиме по умолчанию [9] (это кстати именно тот режим, который используется в Node.js).
Потом происходит закрытие цикла (остановка всех наблюдателей за событиями, наблюдателей сигналов [10], освобождение памяти выделенной под наблюдатели) и освобождение памяти зарезервированной самим циклом. Нас же будет интересовать устройство функции-запуска цикла (uv_run), посмотрим ее исходный код (он не совсем оригинальный, я удалил строки не связанные с режимом по умолчанию, поэтому в примере переменная “mode” нигде не участвует):
Тело функции-запуска, как мы видим, начинается не с цикла “while”, а с вызова uv__loop_alive. В свою очередь, данная функция проверяет наличие активных обработчиков [11] или запросов [12]:
От результата выполнения этой функции будет зависеть запустится ли цикл “while” или нет. В случае отсутствия запросов или обработчиков, функция-запуска просто обновит время выполнения событийного цикла и тут же завершится.
Если же есть что обрабатывать (r != 0) и флаг остановки не установлен (stop_flag == 0), то цикл запустится. И первым действием в итерации цикла будет тоже обновление времени выполнения (uv__update_time).
Следующий шаг в итерации — это запуск таймеров.
Структура событийного цикла содержит, так называемую, кучу [13] таймеров. Функция-запуска таймеров вытягивает из кучи обработчик таймера с наименьшим временем и сравнивает это значение с временем выполнения событийного цикла. В случае, если время таймера меньше, то этот таймер останавливается (удаляется из кучи, его обработчик тоже удаляется из кучи). Далее идет проверка, нужно ли его перезапустить.
В Node.js (JavaScript) у нас есть функции setInterval [14] и setTimeout [15], в терминах libuv это одно и то же — таймер (uv_timer_t) [16], с той лишь разницей, что у интервального таймера выставлен флаг повтора (repeat = 1).
Интересное наблюдение: в случае выставленного флага повтора, функция uv_timer_stop сработает дважды для обработчика таймера.
Перейдем к следующему действию в итерации событийного цикла, а именно функции-запуску ожидающих обратных вызовов (pending callbacks). Вызовы хранятся в очереди [17]. Это могут быть обработчики чтения или записи файла, TCP [18] или UDP [19] соединений, в общем, любых I/O операций [20], ибо тип не особо имеет значение, так как, вы помните, в unix все есть файл [21].
Далее в итерации идут две мистические строки:
На самом деле это тоже функции-запуска обратных вызовов, но они не имеют никакого отношения к I/O. Фактически, это какие-то внутренние подготовительные действия, которые было бы неплохо совершить перед тем, как начинать выполнение внешних операций (имеется в виду I/O). В случае с “Hello World”, таких обработчиков нет, но на сайте есть примеры, где такие обратные вызовы регистрируются.
В данном примере, idle-обработчик ничего не делает, он будет выполняться пока счетчик не достигнет определенного значение. Таким же способом регистрируются и подготовительные обработчики (prepare).
В Node.js (JavaScript) нет эквивалента этим обработчикам, т.е. мы не можем зарегистрировать какой-то обратный вызов, который бы выполнялся именно на одном из этих шагов. Однако, надо сделать одну оговорку, используя process.nextTick [22], мы можем ненамеренно выполнить код на одном из этих шагов, так как эта функция срабатывает непосредственно на текущем этапе событийного цикла, а это, в том числе, может быть и uv__run_idle или uv__run_prepare. Сама же, функция process.nextTick, никакого отношения к библиотеке libuv не имеет.
На эту тему (работа process.nextTick) у меня сохранилась старая, но пока еще актуальная, диаграмма со stackoverflow:
Следующий этап итерации самый интересный — это внешние операции I/O (poll(2) [23]).
Тут я объединил два шага: вычисление времени для выполнения внешней операции и, непосредственно, внешняя операция.
Вычисление времени выполнения внешней операции I/O по реализации схоже с функцией запуска таймеров, так как значение этого времени вычисляется на основе ближайшего таймера. Этим, кстати, и достигается неблокирующая модель (non-blocking poll).
Исходный код функции uv__io_poll достаточно сложный и не маленький. Там ведется многопоточная работа, регистрируются наблюдатели событий, обратные вызовы и ведется работа с файловыми дескрипторами [24].
Я не буду тут приводить код этой функции, картинка вполне отражает суть этой операции:
Следующая операция в очереди команд итерации событийного цикла это uv__run_check. Она по своей сути идентична функциям uv__run_idle и uv__run_prepare, т.е. это запуск обратных вызовов, регистрирующихся по тому же принципу, и вызывающих после внешних операций. Однако, в этом случае, у нас есть возможность регистрации подобных обработчиков из Node.js. Это функция setImmediate [25] (т.е. немедленное выполнение после внешней операции I/O).
Предпоследний шаг — это запуск закрывающихся обработчиков.
Данная функция обходит связанный список [26] закрывающихся обработчиков и пытается завершить закрытие для каждого. Если у обработчика есть специальный обратный вызов на закрытие, то, по окончанию, запускается этот обратный вызов.
И последний шаг итерации, это уже знакомая функция uv__loop_alive. Если данная функция вернет результат отличный от нуля, то событийный цикл запустит новую итерацию.
Если у вас есть какие-то замечания или дополнения, я буду рад их увидеть в комментариях или пишите на artur.basak.devingrodno@gmail.com
Автор: Artur Basak
Источник [34]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/262862
Ссылки в тексте:
[1] кодовую базу библиотеки в части unix: https://github.com/libuv/libuv/tree/v1.x/src/unix
[2] Событийно-ориентированное программирование: https://en.wikipedia.org/wiki/Event-driven_programming
[3] Проблема была связана с большим количество программных потоков: https://en.wikipedia.org/wiki/C10k_problem
[4] Событийный Цикл (Event Loop): https://en.wikipedia.org/wiki/Event_loop
[5] асинхронным вводом/выводом: https://en.wikipedia.org/wiki/Asynchronous_I/O
[6] Функция обратного вызова (Callback): https://en.wikipedia.org/wiki/Callback_(computer_programming)
[7] http://docs.libuv.org: http://docs.libuv.org
[8] структура событийного цикла: http://docs.libuv.org/en/v1.x/loop.html
[9] режиме по умолчанию: http://docs.libuv.org/en/v1.x/loop.html#c.uv_run_mode
[10] сигналов: https://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D0%B3%D0%BD%D0%B0%D0%BB%D1%8B_(UNIX)
[11] обработчиков: http://docs.libuv.org/en/v1.x/handle.html
[12] запросов: http://docs.libuv.org/en/v1.x/request.html
[13] кучу: https://ru.wikipedia.org/wiki/%D0%9A%D1%83%D1%87%D0%B0_(%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85)
[14] setInterval: https://nodejs.org/api/timers.html#timers_setinterval_callback_delay_args
[15] setTimeout: https://nodejs.org/api/timers.html#timers_settimeout_callback_delay_args
[16] таймер (uv_timer_t): http://docs.libuv.org/en/v1.x/timer.html
[17] очереди: https://ru.wikipedia.org/wiki/%D0%9E%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)
[18] TCP: https://ru.wikipedia.org/wiki/Transmission_Control_Protocol
[19] UDP: https://ru.wikipedia.org/wiki/UDP
[20] I/O операций: https://en.wikipedia.org/wiki/Input/output
[21] в unix все есть файл: https://en.wikipedia.org/wiki/Everything_is_a_file
[22] process.nextTick: https://nodejs.org/api/process.html#process_process_nexttick_callback_args
[23] poll(2): https://linux.die.net/man/2/poll
[24] файловыми дескрипторами: https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2%D1%8B%D0%B9_%D0%B4%D0%B5%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D1%80
[25] setImmediate: https://nodejs.org/api/timers.html#timers_class_immediate
[26] связанный список: https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D1%8F%D0%B7%D0%BD%D1%8B%D0%B9_%D1%81%D0%BF%D0%B8%D1%81%D0%BE%D0%BA
[27] LibUV: Design Overview: http://docs.libuv.org/en/v1.x/design.html
[28] Nodejs.org: The Node.js Event Loop, Timers, and process.nextTick(): https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/
[29] Перевод документации Nodejs.org на тему: https://medium.com/devschacht/event-loop-timers-and-nexttick-18579cd122e0
[30] Philip Roberts: What the heck is the event loop anyway?: https://www.youtube.com/watch?v=8aGhZQkoFbQ
[31] RisingStack.com: Understanding Node.js Event Loop: https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/
[32] Nodesource.com: Understanding Node.js Event Loop: https://nodesource.com/blog/understanding-the-nodejs-event-loop/
[33] Mozilla.org: EventLoop: https://developer.mozilla.org/ru/docs/Web/JavaScript/EventLoop
[34] Источник: https://habrahabr.ru/post/336498/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.