Привет, Хабр! Представляю вашему вниманию перевод статьи "Futures — Isolates — Event Loop" автора Didier Boelens об асинхронности и многопоточности в Dart (и Flutter в частности). И о переводе терминов. В устной речи мы я говорим "стримы", "трэды","фьючеры" и т.д., на печати же эти термины выглядят странно и коряво. Вместе с тем, Stream — это Поток, и Thread — Поток. Контекст в данном случае не всегда спасает. Тот же Изолят, на мой вкус, по-русски звучит нормально, поэтому где-то использую написание кириллицей, а где-то оригинальное. Кое-где в скобках после русских слов их английский оригинал, иногда наоборот. В последнее время я часто получаю вопросы, относящиеся к понятиям Future, async, await, Isolate и параллельного выполнения кода. Кроме того, у некоторых разработчиков видны проблемы с пониманием последовательности выполнения их кода. Я решил воспользоваться возможностью и посвятить этим вопросам статью. Заодно попытаюсь избавиться от путаницы, связанной, в основном, с понятиями асинхронности и параллельности. Для начала следует запомнить, что Dart — однопоточный а Flutter использует Dart. Другими словами, если мы рассматриваем синхронную функцию (или метод класса), то она будет единственной исполняемой прямо сейчас до тех пор, пока не завершится. В примере выше, исполнение myBigLoop() не будет прервано ни при каких условиях. Как следствие, если исполнение метода занимает какое-то время, приложение будет блокировано на то время, пока код выполняется. Компонент Dart, который управляет очерёдностью исполнения инструкций, называется Event Loop. Когда вы запускаете Flutter-приложение (и любое Dart-приложение), создается и запускается новый процесс — Thread, в терминах Дарта — Изолят (Isolate). Это единственный процесс, в котором будет выполняться ваше приложение. Когда этот процесс создан, Дарт автоматически выполняет следующие действия: В течение всего времени жизни основного процесса приложения, один единственный
невидимый вам процесс Event Loop будет управлять порядком исполнения вашего
кода, в зависимости от содержимого двух очередей: MicroTask и Event. Event Loop представляет собой "бесконечный" цикл, частота которого
регулируется внутренними "часами". В случае, если не исполняется никакой другой
Dart-код, этот цикл во время каждого "тика" делает примерно следующее: Как видно, очередь MicroTask имеет приоритет перед очередью Event. Так для чего же используются эти очереди? Используется для очень коротких действий, которые должны быть выполнены асинхронно, сразу после завершения какой-либо инструкции перед тем, как передать управление обратно Event Loop. В качестве примера очень короткого действия представьте освобождение ресурса сразу после его закрытия. Так как процесс закрытия может занять какое-то время, вы могли бы написать что-то наподобие: Вообще-то, это не то, что вам придется часто использовать. К примеру, во всем исходном коде Флаттер метод scheduleMicroTask() встречается всего 7 раз. Предпочтительно использовать очередь Event. Используется для планирования операций, которые получают результат от: Фактически, каждый раз при срабатывании внешнего события, соответствующий код ставится в очередь Event. Как только оказывается, что очередь MicroTask пуста, Event Loop берёт первую задачу из очереди Event и исполняет её. Интересно, что Futures также обрабатываются с помощью очереди Event. Future представляет собой задачу, которая выполняется асинхронно и завершается (успешно или с ошибкой) когда-то в будущем. Что происходит, когда вы создаёте экземпляр Future: Код, связанный с экземпляром Future будет исполнен как любой другой Event,
как только Event Loop возьмёт его из очереди. По завершении кода (успешно или с ошибкой) будет вызван метод then()
или catchError() экземпляра Future. Чтобы проиллюстрировать, давайте посмотрим пример: Запустив код, вот что мы увидим в консоли: Подробнее о последовательности выполнения кода: Важно понимать следующее: Когда вы помечаете объявление метода ключевым словом async, для Dart это значит, что: Некоторые разработчики ошибочно полагают, что await приостанавливает исполнение всего потока до того, как завершится [связанный с ним экземпляр Future], но это не так.
Они не понимают особенности работы Event Loop... Чтобы лучше проиллюстрировать это утверждение, возьмём пример и попробуем
разобраться, что будет результатом его исполнения Правильная последовательность вывода такова: Теперь давайте примем, что methodC() из примера выше отвечает за отправку запроса к серверу и получение ответа может занять неопределённое время. Думаю, можно утверждать, что с учётом этого может быть непросто предсказать точную последовательность исполнения кода. Если же мы хотим, чтобы methodD() был исполнен только в самом конце, можно переписать этот код так:  Вывод будет таким: То, что мы добавили ключевое слово await перед Future в methodC() меняет поведение кода. Так же важно помнить: И последний пример. который я хотел разобрать. Что выведет в консоль method1и что — method2? Будет ли результат одинаковым? Ответ: их поведение различается. Суть в том, что method1 использует функцию forEach() для перебора массива. На каждой итерации он вызывает коллбэк, помеченный как async (так что это Future). Коллбэк исполняется до ключевого слова await, после чего помещает оставшийся код в очередь Event. В конце итерации, выполняется следующий оператор: print('end of loop'). По окончании Event Loop обрабатывает 3 коллбэка. Что касается method2, всё запускается внутри одного блока кода и поэтому исполняется (в этом конкретном примере) последовательно — оператор за оператором. Как видите, даже в таком простом примере кода, нужно держать в уме особенности
работы Event Loop... Итак, как же мы может запустить параллельное выполнение кода во Flutter? И возможно ли это вообще? Да, благодаря понятию Isolate (Изолят). Как было упомянуто ранее, понятие Isolate в Dart соответствует общепринятому Thread (поток).  Однако есть серьёзное отличие от привычной трактовки понятия Thread. Также у него свои собственные очереди (MicroTask и Event). Код исполняется в Изоляте независимо от других Изолятов. В этом и состоит модель параллельное выполнение кода в Dart. Можно использовать несколько различных способов, в зависимости от желаемого результата. Этот подход не использует сторонних пакетов (библиотек) и полагается только на низкоуровневый API, предоставляемый Dart. Как я упомянул ранее, Изоляты не делят память и взаимодействуют посредством сообщений, таким образом, нам нужно найти способ организовать "общение" между "вызывающим" блоком кода и вновь созданным Изолятом. Каждый Изолят предоставляет порт, который используется, чтобы передавать сообщения этому Изоляту. Этот порт называется SendPort (мне лично кажется, что это название вводит в заблуждение, поскольку этот порт предназначен для получения/прослушивания, но именно таково официальное именование). Оба, "вызывающий" и "новый Изолят" должны знать порты друг друга, чтобы иметь возможность "общаться". Именно этот процесс рукопожатия показан в примере кода: Теперь, когда у нас есть доступ к порту Изолята, мы может отправить ему сообщение:  Когда нам больше не нужен экземпляр Изолята, хорошо бы освободить ресурс: Вы должно быть заметили, что для "общения" Изолята и "вызывающего" мы используем стримы (Streams). Они имеют тип Single-Listener Stream (стрим с одним слушателем). В случае, когда вам нужно выполнить код в отдельном потоке и нет необходимости в коммуникации с Изолятом по завершении, в Dart есть вспомогательная функция compute, которая На момент написания статьи, важно отметить, что Другими словами, платформенные взаимодействия невозможны в экземплярах Изолятов,
создаваемых вами программно. Пользователи оценивают качество приложения основываясь на разных факторах, таких как: Ваше приложение может удовлетворять всем этим требованиям, но в случае подвисаний при выполнении операций (user experiences lags), велика вероятность столкнуться с недовольством пользователей. Вот несколько вещей, которые вам следует постоянно учитывать при разработке: Другими словами, желательно, насколько это возможно, использовать механизм Futures (напрямую или посредством async-методов) — код этих Futures будет исполнен как только у Event Loop появится "свободное" время. Это даст пользователям ощущение, что работа проводится параллельно (хотя мы-то знаем, что это не так). Еще один фактор, который может помочь вам решить использовать Future или Изолят — это примерное время, необходимое для исполнения вашего кода. Вот несколько хороших кандидатов для использования в Изоляте: Думаю, что понимание того, как функционирует Event Loop очень важно. Так же важно не забывать, что Flutter (Dart) — однопоточный, поэтому, для того, чтобы удовлетворить пользователей, разработчики должны быть уверены, что приложение будет работать насколько возможно плавно. Futures и Изоляты — мощные инструменты, которые помогут достичь этой цели. Удачи! Mobile dev