Юдель Пэн. Часовщик. 1924
«Компьютер?—?это конечный автомат. Потоковое программирование нужно тем, кто не умеет программировать конечные автоматы»
Алан Кокс, прим. Википедия
“Знай свой инструмент”?—?твердят все вокруг и все равно доверяют. Доверяют модулю, доверяют фреймворку, доверяют чужому примеру.
Излюбленный вопрос на собеседованиях по Node.js?—?это устройство Event Loop. И при всем том, очевидном факте, что прикладному разработчику эти знания будут полезны, мало кто пытается самостоятельно погрузиться в устройство событийного цикла. В основном, всех устраивает картинка сверху. Хоть это и похоже на пересказ фильма, который ты не смотрел, а о котором тебе рассказал друг.
Самое сложное, наверное для меня, это признавать свои ошибки и соглашаться с тем, что я чего-то не знаю. Об ошибках не любят говорить и писать. В основном, все любят писать и говорить о своих успехах и хороших историях, человек старается выстроить образ непобедимого героя.
А ведь как правило, ошибки допускаются именно по незнанию, именно из-за поверхностных суждений, из-за того, что кто-то потратил меньше времени, чем нужно, на изучение поставленного вопроса. Очевидность. Я знаю.
Ниже, я попробую описать мое понимание событийного цикла на примере исходного кода libuv (в тандеме с V8, это основа Node.js), а так же я примкну к когорте людей, которые твердят: “Надо знать свой инструмент”.
Кстати последнее, в современных реалиях, становится нелегким занятием. Один только npm насчитывает, на текущий момент, почти полмиллиона модулей, я уже и не говорю про армию репозиториев на github. Но так все устроено, чтобы оставаться на месте, нужно бежать, чтобы сдвинуться с места, нужно бежать в два раза быстрее.
Эта заметка в первую очередь напоминание мне, напоминание быть внимательнее. Читателю же, я рекомендую погрузиться в исходный код самостоятельно, сделать какие-то выводы, а потом вернуться к этому тексту.
Также, описанное ниже?—?это огромная аппроксимация того, что на самом деле происходит под капотом Node.js. Среди многих других, заметка базируется именно на исходном коде libuv. Я буду рассматривать кодовую базу библиотеки в части unix. Код для win будет другим.
Ну и вначале немного фундаментальной терминологии:
Событийно-ориентированное программирование (СОП, Event-Driven Programming / EDP)?—?парадигма программирования, в которой выполнение программы определяется событиями.
Парадигма СОП активно применяется при разработке GUI, однако, применение ей нашлось и на стороне сервера. В 1999 году, обслуживая популярный в то время публичный FTP-сервер Simtel, его администратор Ден Кегель заметил, что узел на гигабитном канале по аппаратным показателям должен был бы справляться с нагрузкой в 10 000 соединений, но программное обеспечение этого не позволяло. Проблема была связана с большим количество программных потоков, каждый из которых создавался на отдельное соединение.
Идея событийного цикла, работающего в одном потоке, решала эту проблему. Подобные реализации есть не только в мире JavaScript (Node.js). К примеру, Asyncio и Twisted в Python, EventMachine и Celluloid в Ruby, Vert.x в Java. Еще один яркий представитель подобной реализации?—? прокси-сервер Nginx.
В основе СОП лежит Событийный Цикл (Event Loop)?—?программная конструкция, которая занимается диспетчеризацией событий и сообщений в программе.
Цикл, в свою очередь, работает с асинхронным вводом/выводом, или неблокирующим вводом/выводом, что является формой обработки данных, который позволяет другим процессам продолжить выполнение до того, как передача будет завершена.
Функция обратного вызова (Callback)?—?возможность передачи исполняемого кода в качестве одного из параметров другого кода. Подобная техника позволяет нам удобно работать с асинхронным вводом/выводом.
“Hello World!”
А теперь начнем с официального примера “Hello World!” сайта http://docs.libuv.org:
Пример прост, резервируется необходимая оперативная память и инициализируется структура событийного цикла, далее он запускается в режиме по умолчанию (это кстати именно тот режим, который используется в Node.js).
Потом происходит закрытие цикла (остановка всех наблюдателей за событиями, наблюдателей сигналов, освобождение памяти выделенной под наблюдатели) и освобождение памяти зарезервированной самим циклом. Нас же будет интересовать устройство функции-запуска цикла (uv_run), посмотрим ее исходный код (он не совсем оригинальный, я удалил строки не связанные с режимом по умолчанию, поэтому в примере переменная “mode” нигде не участвует):
Тело функции-запуска, как мы видим, начинается не с цикла “while”, а с вызова uv__loop_alive. В свою очередь, данная функция проверяет наличие активных обработчиков или запросов:
От результата выполнения этой функции будет зависеть запустится ли цикл “while” или нет. В случае отсутствия запросов или обработчиков, функция-запуска просто обновит время выполнения событийного цикла и тут же завершится.
Если же есть что обрабатывать (r != 0) и флаг остановки не установлен (stop_flag == 0), то цикл запустится. И первым действием в итерации цикла будет тоже обновление времени выполнения (uv__update_time).
Следующий шаг в итерации?—?это запуск таймеров.
Структура событийного цикла содержит, так называемую, кучу таймеров. Функция-запуска таймеров вытягивает из кучи обработчик таймера с наименьшим временем и сравнивает это значение с временем выполнения событийного цикла. В случае, если время таймера меньше, то этот таймер останавливается (удаляется из кучи, его обработчик тоже удаляется из кучи). Далее идет проверка, нужно ли его перезапустить.
В Node.js (JavaScript) у нас есть функции setInterval и setTimeout, в терминах libuv это одно и то же?—?таймер (uv_timer_t), с той лишь разницей, что у интервального таймера выставлен флаг повтора (repeat = 1).
Интересное наблюдение: в случае выставленного флага повтора, функция uv_timer_stop сработает дважды для обработчика таймера.
Перейдем к следующему действию в итерации событийного цикла, а именно функции-запуску ожидающих обратных вызовов (pending callbacks). Вызовы хранятся в очереди. Это могут быть обработчики чтения или записи файла, TCP или UDP соединений, в общем, любых I/O операций, ибо тип не особо имеет значение, так как, вы помните, в unix все есть файл.
Далее в итерации идут две мистические строки:
На самом деле это тоже функции-запуска обратных вызовов, но они не имеют никакого отношения к I/O. Фактически, это какие-то внутренние подготовительные действия, которые было бы неплохо совершить перед тем, как начинать выполнение внешних операций (имеется в виду I/O). В случае с “Hello World”, таких обработчиков нет, но на сайте есть примеры, где такие обратные вызовы регистрируются.
В данном примере, idle-обработчик ничего не делает, он будет выполняться пока счетчик не достигнет определенного значение. Таким же способом регистрируются и подготовительные обработчики (prepare).
В Node.js (JavaScript) нет эквивалента этим обработчикам, т.е. мы не можем зарегистрировать какой-то обратный вызов, который бы выполнялся именно на одном из этих шагов. Однако, надо сделать одну оговорку, используя process.nextTick, мы можем ненамеренно выполнить код на одном из этих шагов, так как эта функция срабатывает непосредственно на текущем этапе событийного цикла, а это, в том числе, может быть и uv__run_idle или uv__run_prepare. Сама же, функция process.nextTick, никакого отношения к библиотеке libuv не имеет.
На эту тему (работа process.nextTick) у меня сохранилась старая, но пока еще актуальная, диаграмма со stackoverflow:
Следующий этап итерации самый интересный?—?это внешние операции I/O (poll(2)).
Тут я объединил два шага: вычисление времени для выполнения внешней операции и, непосредственно, внешняя операция.
Вычисление времени выполнения внешней операции I/O по реализации схоже с функцией запуска таймеров, так как значение этого времени вычисляется на основе ближайшего таймера. Этим, кстати, и достигается неблокирующая модель (non-blocking poll).
Исходный код функции uv__io_poll достаточно сложный и не маленький. Там ведется многопоточная работа, регистрируются наблюдатели событий, обратные вызовы и ведется работа с файловыми дескрипторами.
Я не буду тут приводить код этой функции, картинка вполне отражает суть этой операции:
Следующая операция в очереди команд итерации событийного цикла это uv__run_check. Она по своей сути идентична функциям uv__run_idle и uv__run_prepare, т.е. это запуск обратных вызовов, регистрирующихся по тому же принципу, и вызывающих после внешних операций. Однако, в этом случае, у нас есть возможность регистрации подобных обработчиков из Node.js. Это функция setImmediate (т.е. немедленное выполнение после внешней операции I/O).
Предпоследний шаг?—?это запуск закрывающихся обработчиков.
Данная функция обходит связанный список закрывающихся обработчиков и пытается завершить закрытие для каждого. Если у обработчика есть специальный обратный вызов на закрытие, то, по окончанию, запускается этот обратный вызов.
И последний шаг итерации, это уже знакомая функция uv__loop_alive. Если данная функция вернет результат отличный от нуля, то событийный цикл запустит новую итерацию.
***
Если у вас есть какие-то замечания или дополнения, я буду рад их увидеть в комментариях или пишите на artur.basak.devingrodno@gmail.com
Nodejs.org: The Node.js Event Loop, Timers, and process.nextTick()
Перевод документации Nodejs.org на тему
Philip Roberts: What the heck is the event loop anyway?
RisingStack.com: Understanding Node.js Event Loop
Nodesource.com: Understanding Node.js Event Loop
Mozilla.org: EventLoop
Комментарии (10)
comerc
28.08.2017 15:55-1"Компьютер?—?это конечный автомат". Алан Кокс, прим. Википедия
Блин, раньше считал, что сам это придумал.
dmnBrest
28.08.2017 16:04+2Спасибо за полезную статью! Я не профильный JS разработчик, и у меня возможно нубский вопрос. Чем прикладному JS разработчику буду полезные эти знания в повседневной работе, ну и, соотвественно, почему этот вопрос популярен на собеседованиях. Можете привести примеры из реально жизни?
Fen1kz
28.08.2017 17:26+3Я думаю, что это вопрос на понимание асинхронности и за счет чего она достигается. Ну а в js-жизни асинхронность ой как часто используется.
archik Автор
28.08.2017 19:45+1Поддерживаю ответ Fen1kz
До кучи, этот вопрос, как правило, идет следующим после вопроса о микро и макро задачах, которые как раз js-прикладник и использует в повседневной жизни.
Т.е. из разряда: «Чей обратный вызов сработает раньше setTimeout или setImmediate? Когда стоит применить то, а когда это?»
Или тот же вопрос, но с примером кода, где вы увидите лесенку асинхронных вложенностей из process.nextTick и setTimeout.
QtRoS
29.08.2017 00:48+2По своему опыту собеседования порядка сотни человек скажу — чем больше у кандидата любознательности, тем лучше он себя показывает на практике. Ценный специалист старается понять внутренние механизмы работы системы, чтобы писать оптимальный код, объяснять (избегать?) необычное поведение или, например, искать лазейки. Очень рекомендую не ждать момента «когда это пригодится в реальной жизни», а прямо сейчас сесть и изучить в деталях то, с чем работаете.
pm_wanderer
Что есть инструмент?
До какого уровня абстракции требуется опуститься, чтобы познать его?
В противовес Вашему утверждению, хочу выразить более разумный лозунг — «Делай свое дело эффективно».
Расходуй свое время эффективно. Развивай себя эффективно.
Часовщик на рисунке дает нам ясное представление, к чему приводит узконаправленное развитие личности — старик в свои седые годы вынужден работать больными руками, ибо он все свои силы положил на познание примитивного инструмента. Таким ли видят свое будущее свидетели «текучих абстракций»?)
archik Автор
И тем не менее, когда у вас сломаются часы, вы пойдете к этому старику. Я восторгаюсь и снимаю шляпу перед людьми, которые посвятили жизнь одному ремеслу. Да, они не универсальные солдаты, но настоящие профи и эксперты своего дела. Будь то плотник, кузнец или часовщик…
pm_wanderer
Я пойду к тому, кто сделает приемлемо по соотношению цена/качество)
Ну и чисто экономически не целесообразно специалистам вкладывать все яйца в одну корзину.
Тут как в любом деле — 90 процентов материала изучается по времени столько же, сколько и оставшиеся 10. А так как человек ограничен 24 часами в сутках, то чтобы получить максимальное количество полезных знаний ему необходимо собирать лишь те плоды, что уже созрели и лежат на земле. Это самый оптимальный путь в объективной реальности, что косвенно подтверждает обилие багов в софте, который пишут далеко не глупые люди)
archik Автор
А про абстракции, это, как я понял, отсылка к Джоелю Спольскому. И он, как раз в рассуждениях на этот счет, пример приводит с тем, что не достаточно нанять программиста, который знает только Visual Basic, К&Р тоже может понадобиться. Аналогия с libuv и node.js вполне себе.
pm_wanderer
Абстракции текут, но это не значит что течь должен устранять капитан судна)