вечный луп это что
Как управлять event loop в JavaScript. Часть 1
Event loop (событийные циклы) — важная часть архитектуры JavaScript. Мы попросили эксперта объяснить, как в этом разобраться.
Понимание работы event loop — неотъемлемый пункт карьерного роста middle-разработчика. Если хотите знать всё о JavaScript, ждём вас на курсе «Frontend-разработчик». Вы научитесь верстать сайты и создавать интерфейсы, а на выходе получите два готовых проекта в портфолио.
А сейчас о событийных циклах рассказывает Александр Кузьмин — ведущий программист с десятилетним опытом во frontend, руководитель отдела клиентской разработки компании IT-Park. Передаём слово эксперту.
Автор в сфере IT, digital, экономики и финансов. Ведет некоммерческий проект для начинающих писателей «ЛитЦех».
Асинхронность в JavaScript
У каждого языка свой подход к параллельному вычислению данных. Например, в языках типа C++ оно передаётся в отдельный поток или даже процесс, который выполняется на другой машине.
ведущий программист, руководитель отдела клиентской разработки компании IT-Park
Если нужно сообщить потоку что-то вроде «посчитай вот это и положи результат в базу данных, а я когда-нибудь приду за ними», мы имеем дело с асинхронными операциями.
Это значит, что код, который их вызвал, не ждёт завершения выполнения, а продолжает исполняться дальше. Если же мы хотим дождаться результата, у многих современных языков есть операторы async и await для синхронизации исполняемого кода.
В JavaScript асинхронность — основной инструмент. Во времена до появления Node.JS он был практически единственным языком исполнения сценариев на клиенте в вебе (Internet Explorer поддерживал VB Script, но его никто не использовал). Сейчас невозможно представить интернет, где все запросы на сервер отправлялись бы с перезагрузкой страницы. Напротив, мы пришли к одностраничному вебу, в котором на стороне клиента происходит разрешение адресов страниц и отображение соответствующего контента.
Любые данные от сервера запрашиваются асинхронно: отправляется запрос (XMLHttpRequest или XHR), и код не ждёт его возвращения, продолжая выполняться. Когда же сервер отвечает, объект XHR получает уведомление об этом и запускает функцию обратного вызова — callback, который передали в него перед отправкой запроса.
Если придётся ждать, пока запрос придёт, JavaScript перестанет принимать любые события, а страница зависнет. Чтобы пользователь спокойно использовал веб-приложение, запрос выводят из текущего контекста выполнения. Операции, результата которых приходится ждать, прежде чем продолжать выполнение кода, называются блокирующими. О них — во второй части статьи.
Суть кроется в устройстве языка:
JavaScript ориентирован на общение с пользователем, чтобы тот мог запускать несколько событий одновременно.
Если правильно использовать инструменты языка, то выполнение кода, которое происходит последовательно и в одном потоке, никак не мешает приёму событий и реакции на них — человек спокойно работает с интерфейсом, не замечая лагов, сбоев и зависаний.
Event loop в JavaScript — менеджер асинхронных вызовов
Чтобы этот хитрый процесс слаженно работал, в JavaScript реализован механизм для управления очерёдностью исполнения кода. Поскольку это однопоточный язык, возникла необходимость «вклиниваться» в текущий контекст исполнения. Этот механизм называется event loop — событийный цикл.
С английского loop переводится как «петля», что отлично отражает смысл: мы имеем дело с закольцованной очередью.
Event loop регулирует последовательность исполнения контекстов — стек. Он формируется, когда сработало событие или была вызвана функция. Реакция на событие помещается в очередь исполнения, в event loop, который последовательно, с каждым циклом выполняет попадающий в него код. При этом привязанная к событию функция вызывается следующей после текущего контекста исполнения.
В JavaScript постоянно работают связанные между собой синхронная и асинхронная очереди выполнения. Синхронная — стек — формирует очередь и пробрасывает в асинхронную — event loop — вызовы функций, которые будут выполнены после текущего запланированного исполняемого контекста.
Чтобы данные находились в консистентном состоянии, каждая функция должна быть выполнена до конца. Это обусловлено однопоточностью JavaScript и некоторыми другими особенностями, например характерными для функциональных языков программирования замыканиями. Поэтому единственный поток представлен в виде очереди контекстов исполнения, в которой и происходит «вклинивание» функций, прошедших через цикл событий.
Справка
В JavaScript существует понятие «контекст функции». Но есть и другой термин — «контекст исполнения». Это тело функции со всеми переменными и другими функциями, которое называют «область видимости», с английского — “scope”. Важно не путать понятия, это принципиально разные вещи.
Как формируется контекст исполнения
JavaScript — интерпретируемый язык. Это значит, что любой код проходит через интерпретатор, который исполняет его построчно. Но и здесь есть нюансы.
Как только скрипт попадает в интерпретатор, формируются глобальный контекст и глобальная область видимости, в которой держится Variable Object, или VO — объект переменных.
Он формируется из переменных вида Function Declaration и атрибутов функции по следующему принципу. Интерпретатор считывает код и находит все объявления:
Это складывается в VO текущего контекста исполнения. Затем берётся Variable Object внешней области видимости и к нему добавляется сформированный выше VO. Сверху он дополняется параметрами функции и их значениями на момент исполнения.
При этом нет разницы, в каком месте функции они определяются. Переменная может быть определена в любой части кода, как и функция.
VO этого скрипта формируется:
Затем скрипт начнет исполняться по следующему сценарию:
Теперь перепишем скрипт, добавив setTimeout с нулевым тайм-аутом у вызова функции:
На первый взгляд может показаться, что ничего не изменится и функция func будет выполнена без задержки. Но это не так. На самом деле произойдёт следующее:
Почему так происходит?
Всему виной setTimeout, очевидно. Он выводит контекст исполнения функции из синхронного потока, помещая его в event loop. То же самое происходит и с регистрацией событий. Мы можем подписаться на событие при помощи функции addEventListener. Передавая функцию обратного вызова — callback, добавляем её в список функций, которые должны быть вызваны при срабатывании этого события.
Допустим, мы хотим нажатием на кнопку перекрасить её в красный цвет. Код, который это выполняет, выглядит так:
Переданная функция всегда выполняется через event loop. При возникновении события в цикл последовательно попадут все привязанные к нему функции. Для каждой будет формироваться контекст исполнения, который будет запущен следом за текущим.
Если в процессе будет вызвано ещё одно событие, его коллбэки будут вставать в очередь по тому же принципу, возможно, перемежаясь с контекстами исполнения первого события.
Более сложный пример: есть две кнопки, первая перекрашивает фон страницы в красный цвет, а вторая — в жёлтый, но у второй перекрашивание фона завёрнуто в setTimeout с нулевой задержкой. И мы вручную вызываем событие нажатия сначала на жёлтую кнопку, а потом — на красную.
Что происходит в такой ситуации, исходя из того, что мы рассмотрели выше?
Обратите внимание, что исполнение коллбэков событий click на кнопках при вызове из кода происходит сразу же, не попадая в event loop: setTimeout с нулевой задержкой отложил перекраску фона в жёлтый, но функция сама была исполнена в момент вызова.
Это происходит из-за того, что события из кода не требуется выполнять асинхронно. Действительно, в такой ситуации мы находимся в предсказуемом окружении, тогда как пользовательские события могут случаться в любой момент.
Это приводит к теме следующей части: об управлении событийным циклом и тем, что будет в него попадать. Подробно рассмотрим, как грамотно формировать цепочки последовательных асинхронно вызванных контекстов вызова, уменьшая вычислительную сложность каждого из них, освобождая поток и позволяя пользовательским событиям «вклиниваться» в очередь исполнения.
Если вы хотите знать все о JavaScript, ждем вас на курсе «Профессия frontend-разработчик». Вы научитесь верстать сайты и создавать интерфейсы, а на выходе получите два готовых проекта в портфолио.
Как писать эффективный код на JavaScript с помощью Event Loop
Авторизуйтесь
Как писать эффективный код на JavaScript с помощью Event Loop
Препарирует принципы работы Event Loop Евгений, старший разработчик Noveo
Event Loop (цикл событий) — один из важнейших аспектов в JavaScript, знание которого позволяет писать более эффективный код. В статье мы рассмотрим, как работает основной поток в JavaScript и как он обрабатывает асинхронные функции.
Долгое время я писал код на JavaScript, не до конца понимая, как он работает под капотом. В принципе, для того чтобы кодить на JavaScript, знать принципы его работы изнутри и не нужно, но это сделает ваш код лучше и позволит взглянуть на некоторые вещи в языке под другим углом.
Весь код в JavaScript выполняется в одном потоке, т. е. за один раз может обрабатываться только что-то одно. С одной стороны, это полезное ограничение, так как оно позволяет нам не задумываться об особенностях работы с параллелизмом. С другой стороны, мы постоянно должны контролировать код и заботиться о том, чтобы синхронные операции (бесконечные циклы, запрос данных по сети) не блокировали наш поток.
В большинстве браузеров у каждой вкладки свой цикл событий, это позволяет изолировать вкладки друг от друга, и если одна зависнет, другие продолжат работать. В противном случае одна зависшая операция могла бы нарушить работу всего браузера во всех вкладках.
По факту окружение может одновременно управлять большим количеством «циклов событий» для обработки API-запросов. WebWorkers также имеют свой цикл событий.
JavaScript-разработчик должен знать, что его код всегда выполняется в одном цикле событий, и следить за тем, чтобы не заблокировать его.
Блокирование Event Loop
Любой код, который будет долго выполняться прежде чем вернёт управление в основной поток, блокирует выполнение любого JavaScript-кода на странице. При этом также блокируется пользовательский интерфейс, и пользователь не может с ним взаимодействовать (кликать, прокручивать страницу и т. д.).
Почти все операции ввода/вывода в JavaScript являются неблокирующими — сетевые запросы, операции с файловой системой в Node.js и т. д. Исключением являются блокирующие операции, и именно поэтому в JavaScript так популярны обратные вызовы (callbacks), а в последнее время всё чаще начинают использовать Promise и async/await.
Стек вызовов
Стек вызовов — это очередь LIFO (Last In, First Out).
Цикл событий непрерывно обрабатывает стек вызовов в поиске функции, которая должна быть обработана. При этом он добавляет любой найденный вызов функции в стек вызовов и выполняет каждый по порядку.
Если вы знакомы со стеком вызовов в отладчике или консоли браузера, то пример далее будет вам понятен. Браузер ищет имена функций в стеке вызовов, чтобы сообщить вам, какая функция инициирует текущий вызов:
Примеры работы с Event Loop
На небольшом примере мы рассмотрим, как работает Event Loop:
Имена функций bar, baz, foo взяты в качестве примера.
После выполнения этот код выведет:
В принципе, как и ожидалось.
В этот момент стек вызовов выглядит так:
Цикл обработки событий на каждой итерации проверяет, есть ли в стеке вызовы, и если да, выполняет их:
Этот процесс продолжается до тех пор, пока стек не станет пустым.
Порядок выполнения функций
В примере выше нет ничего специфичного: JavaScript анализирует код и определяет порядок вызова функций.
Давайте посмотрим, как мы можем изменить порядок вызова функций, сделав так, что определённая функция будет вызвана последней. Для этого мы выполним вызов нашей функции посредством browser API:
Рассмотрим следующий пример:
Результат выполнения этого кода для некоторых может стать неожиданным:
На этом этапе стек вызовов выглядит следующим образом:
Порядок вызова функций в этом случае будет выглядеть так:
Далее мы рассмотрим, почему так происходит.
Очередь сообщений (The Message Queue)
В первую очередь Event Loop обрабатывает всё, что содержится в стеке вызовов, и только после этого начинает обрабатывать содержимое очереди.
Очередь заданий (ES6 Job Queue)
ECMAScript 2015 представил концепцию очереди заданий, которая используется в Promises (также представлена в ES6/ES2015). Это способ выполнить результат асинхронной функции как можно скорее, а не помещать его в конец стека вызовов. Обещания, которые разрешаются до завершения текущей функции, будут выполняться сразу после текущей функции.
Это своего рода VIP-очередь, обработка которой имеет приоритет по отношению к обычной очереди.
В этом большая разница между Promises (а также async/await, который построен на Promises) и привычными асинхронными функциями через setTimeout() или другие API-платформы.
Надеемся, статья поможет вам разобраться с работой Event Loop в JavaScript, включая работу с потоками, очередями событий и API браузера. Для наглядности мы рассмотрели несколько примеров и то, как их можно оптимизировать с точки зрения производительности.
Что ты такое, Event Loop? Или как устроен цикл событий в браузере Chrome
Как думаете, что произойдет, если запустить в консоли браузера этот фрагмент кода?
Если вы также, как и я, прочитали кучу статей про Event Loop, Main Thread, таски, микротаски и прочее, но затрудняетесь ответить на вопросы выше — эта статья для вас.
Итак, приступим. Код каждой HTML-страницы в браузере выполняется в Main Thread. Main Thread — это основной поток, где браузер выполняет JS, делает перерисовки, обрабатывает пользовательские действия и многое другое. По сути, это то место, где движок JS интегрирован в браузер.
Проще всего разобраться, глядя на схему:
Рисунок 1
Мы видим, что единственное место, через которое задачи могут попасть в Call Stack и выполниться — это Event Loop. Представьте, что вы оказались на его месте. И ваша работа успевать ‘разгребать’ задачи. Задачи могут быть двух типов:
Конечно, первое, что приходит в голову — задать каждому заказчику приоритет, и выстроить их в очередь. Второе — определить, как именно будут обрабатываться задачи от каждого заказчика — по одной, все сразу или, может быть, пачками.
Взглянем на эту схему:
Рисунок 2
На основе этой схемы строится вся работа Event Loop. После того как Call Stack станет пустым (закончились личные задачи), Event Loop первым делом идет к заказчику Tasks и просит у него только одну, первую в очереди задачу, передает ее в CallStack и идет дальше. Следующий заказчик — Microtasks. У него Event Loop берет задачи до тех пор, пока они не закончатся. Это значит, что если время их добавления равно времени их выполнения, то Event Loop будет бесконечно их разгребать.
Далее он идет к Render и выполняет задачи от него. Задачи от Render оптимизируются браузером и, если он посчитает, что в этом цикле не нужно ничего перерисовывать, то Event Loop просто пойдет дальше.
После Render цикл снова повторяется и так пока вкладка браузера не закроется и не завершится процесс.
Если у кого-то из заказчиков не оказалось задач, то Event Loop просто идет к следующему. И, наоборот, если у заказчика задачи занимают много времени, то остальные заказчики будут ждать своей очереди. А если задачи от какого-то заказчика оказались бесконечными, то Call Stack переполняется, и браузер начинает ругаться:
Рисунок 3
Теперь, когда мы поняли как работает Event Loop, пришло время разобраться, что будет после выполнения фрагментов кода в начале этой статьи.
Мы видим, что функция foo вызывает сама себя рекурсивно через setTimeout внутри, но при каждом вызове она создает задачу заказчика Tasks. Как мы помним, в цикле Event Loop при выполнении очереди задач от Tasks берет только 1 задачу в цикл. И далее происходит выполнение задач от Microtasks и Render. Поэтому этот фрагмент кода не заставит Event Loop страдать и вечно разгребать его задачи. Но будет подкидывать новую задачу для заказчика Tasks на каждом круге.
Давайте попробуем выполнить этот скрипт в браузере Google Chrome. Для этого я создал простой HTML-документ и подключил в нем script.js с этим фрагментом кода. После открытия документа заходим в инструменты разработчика, и открываем вкладку Perfomance и жмем там кнопку ‘start profiling and reload page’:
Рисунок 4
Видим, что задачи от Tasks выполняются по одной в цикл, примерно раз в 4ms.
Рассмотрим вторую задачку:
Здесь мы видим тоже самое, что и в примере выше, но вызов foo добавляет задачи от Microtasks, а они выполняются все, пока не закончатся. А это значит, что пока Event Loop не закончит их, перейти к следующему заказчику он не сможет 🙁 И мы видим снова грустную картинку.
Взглянем на это в интрументах разработчкика:
Рисунок 5
Мы видим, что микротаски выполняются примерно раз в 0.1ms, и это в 40 раз быстрее, чем очередь Tasks. Все потому, что они выполняются все и сразу. В нашем примере очередь движется бесконечно. Для визуализации я уменьшил ее до 100 000 итераций.
Надеюсь, эта статья была вам полезной, и теперь вы понимаете, как работает Event Loop, и что ‘творится’ в примерах кода выше.
Всем пока 🙂 И до новых встреч. Если вам понравилось, ставьте лайки и подписывайтесь на мой канал 🙂
Знай свой инструмент: Event Loop в libuv
Юдель Пэн. Часовщик. 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. В свою очередь, данная функция проверяет наличие активных обработчиков или запросов:
Следующий шаг в итерации — это запуск таймеров.
Структура событийного цикла содержит, так называемую, кучу таймеров. Функция-запуска таймеров вытягивает из кучи обработчик таймера с наименьшим временем и сравнивает это значение с временем выполнения событийного цикла. В случае, если время таймера меньше, то этот таймер останавливается (удаляется из кучи, его обработчик тоже удаляется из кучи). Далее идет проверка, нужно ли его перезапустить.
В 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. Если данная функция вернет результат отличный от нуля, то событийный цикл запустит новую итерацию.