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

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

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


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

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

    image

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

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

    image

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

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

    image

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

    image

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

    image

    image

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

    image

    Структура событийного цикла содержит, так называемую, кучу таймеров. Функция-запуска таймеров вытягивает из кучи обработчик таймера с наименьшим временем и сравнивает это значение с временем выполнения событийного цикла. В случае, если время таймера меньше, то этот таймер останавливается (удаляется из кучи, его обработчик тоже удаляется из кучи). Далее идет проверка, нужно ли его перезапустить.

    В Node.js (JavaScript) у нас есть функции setInterval и setTimeout, в терминах libuv это одно и то же — таймер (uv_timer_t), с той лишь разницей, что у интервального таймера выставлен флаг повтора (repeat = 1).

    Интересное наблюдение: в случае выставленного флага повтора, функция uv_timer_stop сработает дважды для обработчика таймера.

    Перейдем к следующему действию в итерации событийного цикла, а именно функции-запуску ожидающих обратных вызовов (pending callbacks). Вызовы хранятся в очереди. Это могут быть обработчики чтения или записи файла, TCP или UDP соединений, в общем, любых I/O операций, ибо тип не особо имеет значение, так как, вы помните, в unix все есть файл.

    image

    image

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

    image

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

    image

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

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

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

    image

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

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

    image

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

    image

    image

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

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

    image

    image

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

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

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

    image

    image

    И последний шаг итерации, это уже знакомая функция uv__loop_alive. Если данная функция вернет результат отличный от нуля, то событийный цикл запустит новую итерацию.

    ***


    Если у вас есть какие-то замечания или дополнения, я буду рад их увидеть в комментариях или пишите на artur.basak.devingrodno@gmail.com

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 19

      –1
      Знай свой инструмент


      Что есть инструмент?
      До какого уровня абстракции требуется опуститься, чтобы познать его?
      В противовес Вашему утверждению, хочу выразить более разумный лозунг — «Делай свое дело эффективно».

      Расходуй свое время эффективно. Развивай себя эффективно.

      Часовщик на рисунке дает нам ясное представление, к чему приводит узконаправленное развитие личности — старик в свои седые годы вынужден работать больными руками, ибо он все свои силы положил на познание примитивного инструмента. Таким ли видят свое будущее свидетели «текучих абстракций»?)
        0
        И тем не менее, когда у вас сломаются часы, вы пойдете к этому старику. Я восторгаюсь и снимаю шляпу перед людьми, которые посвятили жизнь одному ремеслу. Да, они не универсальные солдаты, но настоящие профи и эксперты своего дела. Будь то плотник, кузнец или часовщик…
          0
          Я пойду к тому, кто сделает приемлемо по соотношению цена/качество)
          Ну и чисто экономически не целесообразно специалистам вкладывать все яйца в одну корзину.

          Тут как в любом деле — 90 процентов материала изучается по времени столько же, сколько и оставшиеся 10. А так как человек ограничен 24 часами в сутках, то чтобы получить максимальное количество полезных знаний ему необходимо собирать лишь те плоды, что уже созрели и лежат на земле. Это самый оптимальный путь в объективной реальности, что косвенно подтверждает обилие багов в софте, который пишут далеко не глупые люди)
          0
          А про абстракции, это, как я понял, отсылка к Джоелю Спольскому. И он, как раз в рассуждениях на этот счет, пример приводит с тем, что не достаточно нанять программиста, который знает только Visual Basic, К&Р тоже может понадобиться. Аналогия с libuv и node.js вполне себе.
            0
            Абстракции текут, но это не значит что течь должен устранять капитан судна)
          –1

          "Компьютер — это конечный автомат". Алан Кокс, прим. Википедия


          Блин, раньше считал, что сам это придумал.

            +2
            Спасибо за полезную статью! Я не профильный JS разработчик, и у меня возможно нубский вопрос. Чем прикладному JS разработчику буду полезные эти знания в повседневной работе, ну и, соотвественно, почему этот вопрос популярен на собеседованиях. Можете привести примеры из реально жизни?
              +3
              Я думаю, что это вопрос на понимание асинхронности и за счет чего она достигается. Ну а в js-жизни асинхронность ой как часто используется.
                +1
                Поддерживаю ответ Fen1kz
                До кучи, этот вопрос, как правило, идет следующим после вопроса о микро и макро задачах, которые как раз js-прикладник и использует в повседневной жизни.
                Т.е. из разряда: «Чей обратный вызов сработает раньше setTimeout или setImmediate? Когда стоит применить то, а когда это?»
                Или тот же вопрос, но с примером кода, где вы увидите лесенку асинхронных вложенностей из process.nextTick и setTimeout.
                  0
                  К вопросу про «микро и макро задачи». Не знал, теперь буду иметь в виду. Но опять же погружаясь в тему у меня возник вопрос. Вот тут в статье (правда за 2015 год) автор отлично на примере показывает отличие микро и макро задач, но тут же показывает что браузеры каждый по своему интерпретирует эту логику. Получается знать хорошо, но бесполезно? Получается что ноги растут из того же event loop, который в каждом движке отличается по своей реализации? Как дела обстоят с поддержкой на сегодняшний день?
                    +2
                    Статья Джейка хороша, все верно, у каждого браузера свой js-движок, и реализация event loop своя, тут же речь о платформе Node.js, т.е. это не про браузеры, а про серверную js-технология. Она использует V8 (js-движок, тот же, что и в Chrome) и libuv для организации event loop и работы с файлами, сокетами, сетью, ОС и т.е.

                    К примеру, функций setImmediate (разве что нестандартизированная в FF) и process.nextTick в брузерах не найти, они характерны только Node.js платформе и ее событийному циклу.

                    libuv используется не только в node.js, есть и другие платформы.

                    Полезно ли знать это или нет — решать вам, решать каждому, я считаю, что полезно, затем и опубликовал заметку.
                      +1
                      Спасибо что разъяснили. Моя невнимательность меня подвела. Я совсем не придал значение упоминанию в статья NodeJS, зато сразу ухватился на «javascript». Но собственно и доказывать что в информации нет пользы не было цели. Я хотел лишь увидеть практической значение этих знаний. Кое-что я для себя открыл! :) Спасибо!
                  +2
                  По своему опыту собеседования порядка сотни человек скажу — чем больше у кандидата любознательности, тем лучше он себя показывает на практике. Ценный специалист старается понять внутренние механизмы работы системы, чтобы писать оптимальный код, объяснять (избегать?) необычное поведение или, например, искать лазейки. Очень рекомендую не ждать момента «когда это пригодится в реальной жизни», а прямо сейчас сесть и изучить в деталях то, с чем работаете.
                  0

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

                    0

                    Ну и что то менеджер пакетов насчитывает столько модулей. Это разве заставляет вас их все учить?) вообще не понял про бежать в х2 быстрее к чему здесь)
                    Хорошая статья

                      0

                      Но после прочтения остаётся ряд вопросов. Что такое время таймера и что такое время цикла — не обозначается. Когда говорится об определении времени выполнения операции ввода вывода — тоже непонятно. Разве есть такие механизмы ядра, которые могут нам сказать, сколько времени будет выполняться асинхронная операция?) — ответ нет.
                      Потом что такое idle?) Дальше непонятно про что то вроде «благо мы знаем что в никсах все файл и тип одинаковый». Какая разница? Мы просто имеем дескриптор на сущность. В винде то же самое.

                        0
                        А мне ненравится, как в libuv сделаны таймера. Казалось бы — ну возьмите timerfd (для винды же тоже аналоги есть?), у них и прецезионность выше, и ненадо мудрить с таймаутами для epoll_wait(), и они поддерживают эвэнты не только по относительному — но и по абсолютному времени, и часы поддерживаются какие хочешь (монотонные, настенные...). И все это вокруг файлового дескриптора, то есть, делать вообще ничего ненадо.
                        На фоне этого, мудерж с таймаутами для epoll_wait() выглядит костылем.
                          0

                          На винде waitable timer только блокирующие. Кроме того, это же +1 системный вызов на операцию...

                            0
                            Ну, не знаю. Тогда, может и правильно, потому что при надобности timerfd пристыковывается при помощи структурки uv_poll_t.

                            Но при той реализации таймеров, которая в libuv — там эти таймера получаются не очень точные. И, насколько понимаю, при такой реализации у периодических таймеров не будет ни fixed rate ни fixed delay — а будет нечто среднее.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое