- PVSM.RU - https://www.pvsm.ru -

Знай свой инструмент: Event Loop в libuv

image
Юдель Пэн. Часовщик. 1924

«Компьютер — это конечный автомат. Потоковое программирование нужно тем, кто не умеет программировать конечные автоматы» 
Алан Кокс, прим. Википедия

“Знай свой инструмент” — твердят все вокруг и все равно доверяют. Доверяют модулю, доверяют фреймворку, доверяют чужому примеру.

Излюбленный вопрос на собеседованиях по Node.js — это устройство Event Loop. И при всем том, очевидном факте, что прикладному разработчику эти знания будут полезны, мало кто пытается самостоятельно погрузиться в устройство событийного цикла. В основном, всех устраивает картинка сверху. Хоть это и похоже на пересказ фильма, который ты не смотрел, а о котором тебе рассказал друг.

image

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

Ниже, я попробую описать мое понимание событийного цикла на примере исходного кода 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!”

А теперь начнем с официального примера “Hello World!” сайта http://docs.libuv.org [7]:

image

Пример прост, резервируется необходимая оперативная память и инициализируется структура событийного цикла [8], далее он запускается в режиме по умолчанию [9] (это кстати именно тот режим, который используется в Node.js). 

Потом происходит закрытие цикла (остановка всех наблюдателей за событиями, наблюдателей сигналов [10], освобождение памяти выделенной под наблюдатели) и освобождение памяти зарезервированной самим циклом. Нас же будет интересовать устройство функции-запуска цикла (uv_run), посмотрим ее исходный код (он не совсем оригинальный, я удалил строки не связанные с режимом по умолчанию, поэтому в примере переменная “mode” нигде не участвует):

image

Тело функции-запуска, как мы видим, начинается не с цикла “while”, а с вызова uv__loop_alive. В свою очередь, данная функция проверяет наличие активных обработчиков [11] или запросов [12]:

image

От результата выполнения этой функции будет зависеть запустится ли цикл “while” или нет. В случае отсутствия запросов или обработчиков, функция-запуска просто обновит время выполнения событийного цикла и тут же завершится.
Если же есть что обрабатывать (r != 0) и флаг остановки не установлен (stop_flag == 0), то цикл запустится. И первым действием в итерации цикла будет тоже обновление времени выполнения (uv__update_time).

image

image

Следующий шаг в итерации — это запуск таймеров.

image

Структура событийного цикла содержит, так называемую, кучу [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].

image

image

Далее в итерации идут две мистические строки:

image

На самом деле это тоже функции-запуска обратных вызовов, но они не имеют никакого отношения к I/O. Фактически, это какие-то внутренние подготовительные действия, которые было бы неплохо совершить перед тем, как начинать выполнение внешних операций (имеется в виду I/O). В случае с “Hello World”, таких обработчиков нет, но на сайте есть примеры, где такие обратные вызовы регистрируются.

image

В данном примере, idle-обработчик ничего не делает, он будет выполняться пока счетчик не достигнет определенного значение. Таким же способом регистрируются и подготовительные обработчики (prepare).

В Node.js (JavaScript) нет эквивалента этим обработчикам, т.е. мы не можем зарегистрировать какой-то обратный вызов, который бы выполнялся именно на одном из этих шагов. Однако, надо сделать одну оговорку, используя process.nextTick [22], мы можем ненамеренно выполнить код на одном из этих шагов, так как эта функция срабатывает непосредственно на текущем этапе событийного цикла, а это, в том числе, может быть и uv__run_idle или uv__run_prepare. Сама же, функция process.nextTick, никакого отношения к библиотеке libuv не имеет.

На эту тему (работа process.nextTick) у меня сохранилась старая, но пока еще актуальная, диаграмма со stackoverflow:

image

Следующий этап итерации самый интересный — это внешние операции I/O (poll(2) [23]).

Тут я объединил два шага: вычисление времени для выполнения внешней операции и, непосредственно, внешняя операция.

image

Вычисление времени выполнения внешней операции I/O по реализации схоже с функцией запуска таймеров, так как значение этого времени вычисляется на основе ближайшего таймера. Этим, кстати, и достигается неблокирующая модель (non-blocking poll).

image

image

Исходный код функции uv__io_poll достаточно сложный и не маленький. Там ведется многопоточная работа, регистрируются наблюдатели событий, обратные вызовы и ведется работа с файловыми дескрипторами [24].

Я не буду тут приводить код этой функции, картинка вполне отражает суть этой операции:

image

image

Следующая операция в очереди команд итерации событийного цикла это uv__run_check. Она по своей сути идентична функциям uv__run_idle и uv__run_prepare, т.е. это запуск обратных вызовов, регистрирующихся по тому же принципу, и вызывающих после внешних операций. Однако, в этом случае, у нас есть возможность регистрации подобных обработчиков из Node.js. Это функция setImmediate [25] (т.е. немедленное выполнение после внешней операции I/O).

Предпоследний шаг — это запуск закрывающихся обработчиков.

Данная функция обходит связанный список [26] закрывающихся обработчиков и пытается завершить закрытие для каждого. Если у обработчика есть специальный обратный вызов на закрытие, то, по окончанию, запускается этот обратный вызов.

image

image

И последний шаг итерации, это уже знакомая функция 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