Почему была создана эта статья? Из-за того что ранее были поступили некоторые «вопросы» относительно «Интересных асинхронных вопросов в интервью», но объяснить эти вопросы за несколько слов было затруднительно, поэтому было решено написать полное объяснение. Надеюсь, данная статья поможет вам полностью понять реализацию асинхронной работы Dart в Flutter.
⚠️ Данная статья может содержать большое количество информации, основана на Flutter 3.22+ и Dart 3.4+, пожалуйста, будьте терпеливы при чтении.
Если вы хотите объяснить механизм асинхронной работы в Dart, то обязательно придется упомянуть очереди (isolates). Для полного понимания асинхронной работы важно начинать с изучения очередей, так как каждый Dart-код выполняется внутри определённого isolate, например, наш входной метод main
выполняется внутри корневого isolate, который можно рассматривать как главный поток Dart-кода. Код Dart в одном isolate работает в однопоточной модели, используя цикл событий для реализации асинхронных операций. С точки зрения разработчика, каждый isolate можно рассматривать как отдельный поток, хотя строго говоря это неверно.
Рассказывая об isolates, нельзя обойтись без этой старой диаграммы. Давайте рассмотрим некоторые базовые концепции:- Каждый isolate имеет своё глобальное состояние (global state), рабочий поток (mutator thread) и вспомогательный поток (helper thread).
Как объекты, реализующие параллелизм, isolates, как следует из их названия, представляют собой "изоляцию". Они работают независимо друг от друга, не могут использовать общую память и взаимодействуют между собой только через порты (ports).
Группы isolates были введены в Dart 2.15. Isolates в группах isolates совместно используют различные внутренние структуры данных текущего запущенного приложения.
После Dart 2.15 запуск дополнительных isolates в группах isolates стал быстрее примерно в 100 раз, поскольку теперь нет необходимости инициализировать структуры программы, а также объём необходимой памяти для создания новых isolates уменьшился в 10-100 раз.
Метод
Isolate.spawn
позволяет создать новый isolate в рамках одной группы, аIsolate.spawnUri
— запустить новую группу. Хотя группы изолируемых объектов всё ещё не позволяют изоляциям делиться изменяемыми объектами, внутри группы можно использовать общую область памяти, что позволяет раскрыть больше возможностей, например, передача объекта из одного изолята в другой, а не только базовых типов.Итак, основные концепции изолятов и групп изолятов должны быть поняты, теперь давайте обсудим взаимоотношения между изолятом и внешним миром.
Мы знаем, что изоляты могут выполнять операции с потоками, но они всё ещё зависят от системных потоков для выполнения задач. Тогда вопрос: есть ли одно-к-одному соотношение между системными потоками и изолятами? Ответ — нет.
Простое объяснение:
Несколько менее очевидных отношений:
Поэтому изоляты и системные потоки точно не имеют отношения одно-к-одному; фактически, внутренняя виртуальная машина Dart использует пулы потоков (ThreadPools) для управления системными потоками, а изоляты не являются постоянными "мертвыми" циклами на потоках, и код Dart VM реализован вокруг логики ThreadPool::Task, а не системных потоков.> Например, когда изолят обрабатывает цикл событий, он отправляет MessageHandlerTask
в пулы потоков, после чего выбирается свободный поток или создаётся новый поток для выполнения этой задачи. После завершения работы поток может продолжить работу над другими задачами изолята, в зависимости от конкретной ситуации.
Таким образом, можно сделать вывод, что все программы Dart выполняются внутри изолятов, а не непосредственно в потоках, хотя каждый изолят может использовать потоки из пула потоков для обработки циклов событий.
На следующих диаграммах показана приближенная связь между изолятами и потоками, хотя она и не является строго научной, но достаточно простой для понимания.
Концепция запускатора может показаться незнакомой, но, например, в Xcode имя проекта по умолчанию часто называется Runner. Однако запускатор является абстрактной концепцией в Flutter и не имеет прямого отношения к изолятам.
Для Flutter Engine он может отправлять задачи в запускатор, поэтому его также называют TaskRunner
. Например, в Flutter есть четыре TaskRunner
(UI, GPU, IO, Platform).
Для Flutter Engine важно знать, что он не заботится о том, какой именно поток используется для выполнения TaskRunner. Однако самому TaskRunner лучше всегда работать в одном и том же потоке. Например, Android и iOS создают отдельные потоки для UI, GPU и IO. В частности, UI TaskRunner представляет собой Dart root isolate, то есть основной поток Dart.> Что касается Platform Runner, это главный поток платформы устройства. На мобильных платформах этот поток является общим, и все экземпляры Engine используют один и тот же Platform Runner и поток.
Поэтому в Flutter:
Хотя Runner и isolates не имеют прямого отношения, между ними всё равно существует "взаимодействие", например, между root isolate и UI Runner.
По нашему мнению, UI Runner представляет собой UI поток Dart в Flutter, а root isolate — это основной поток Dart-кода. Таким образом, очевидно, что UI Runner и root isolate находятся в одном и том же потоке.
Чтобы узнать больше об этом выводе, можно рассмотреть процесс запуска Flutter Engine, как показано ниже. Это полный процесс создания root isolate, ключевой момент которого заключается в методе SetMessageHandlingTaskRunner
.
Engine::Run -> RuntimeController::LaunchRootIsolate -> DartIsolate::CreateRunningRootIsolate -> DartIsolate::CreateRootIsolate -> DartIsolate::CreateDartIsolateGroup -> DartIsolate::InitializeIsolate -> DartIsolate::Initialize -> SetMessageHandlingTaskRunner
Для root isolate он связывается с UITaskRunner, а SetMessageHandlingTaskRunner
обеспечивает выполнение root isolate в потоке UITaskRunner. Как показано ниже, задачи root isolate теперь выполняются в потоке UITaskRunner.Поэтому root isolate отличается от других isolate тем, что он не имеет пула потоков, поэтому его очередь сообщений постоянно работает в UI runner'е, с этой точки зрения связь между isolate и runner начинается.Dart isolate и Flutter Runner взаимодействуют через механизм распределения задач, например:
Таким образом, отношения между isolate и Runner становятся более понятными: isolate — это концепция Dart VM, а Runner — концепция Flutter; теоретически они не должны зависеть друг от друга, но root isolate совместно использует один поток с UI Runner.
Наконец, нельзя не упомянуть background isolate. До версии Flutter 3.7 только root isolate мог общаться с платформой через Platform Channels, причины этого мы уже примерно поняли. А начиная с Flutter 3.7, Flutter использует новый BinaryMessenger для обеспечения возможности прямого общения non-root isolate с Platform Channels, конечно, здесь background isolate должен установить связь с root isolate через токены.
Ранее мы кратко рассмотрели, что isolate обрабатывает цикл событий внутри пула потоков, однако для isolate это не постоянный "мертвый" цикл на потоках, а процесс, управляемый событиями, и наличие событий само собой подразумевает наличие очередей событий.
Однако в Dart можно разделить типы очередей на две основные категории: микрозадачи (MicroTask) и события (Event).В цикле обработки очередей Dart сначала рассматривает обработку очереди микрозадач, если она пуста, то переходит к обработке очереди событий. Эта механика позволяет гарантировать, что операции асинхронной микрозадачи будут выполняться до событий пользователя.
Для Flutter:
Главное различие между MicroTask и Event заключается в том, что MicroTask представляет собой минимальную единицу работы, которая должна быть выполнена асинхронно, но не запущена внешними событиями. Это станет очевидным после сравнения с Future. Что касается отношения между микротасками и очередями событий, давайте рассмотрим пример с приведенным ниже кодом:
test() async {
print('начинается');
Future(() => print('Это новый Future'));
Future.microtask(() => print('Это микротаск'));
print('завершается');
}
Как показано на следующей диаграмме, хотя микротаск был добавлен позже, он имеет более высокий приоритет по сравнению со стандартной задачей события, поэтому он будет выполнен первым.
Почему мы не используем микротаски чаще, если они имеют более высокий приоритет? Это связано с тем, что Flutter "настраивает" микротаски таким образом, чтобы они использовали ресурсы движка.
Кроме того, в движке Flutter есть несколько хитростей относительно выполнения микротасков:
BeginFrame
выполняются микротаски;UIDartState
, который добавляет TaskObserver
в MessageLoop
.Из этого можно сделать вывод, что если очередь микротасков слишком длинная, это может вызвать проблемы с отрисовкой кадров и замедлением работы приложения, так как это влияет на цикл обработки кадров и задач UI runner.
Таким образом, мы теперь лучше понимаем реализацию очередей событий и микротасков внутри isolate. Давайте обсудим использование этих механизмов подробнее.
Почему после обсуждения очередей событий мы переходим к обсуждению Timer
? Потому что, помимо операций типа Timer.periodic
, фактически Future
и Future.delayed
также являются типами операций таймера. Можно сказать, что большинство асинхронных действий в Dart основаны на Timer
.Конечно, Future
и Future.delayed
отличаются по своей логике реализации: один требует задержки, другой — нет. Поэтому логика обработки Dart для этих двух случаев различна. Для асинхронных событий типа Future
, которые не требуют таймера (_ZERO_EVENT
), они фактически обрабатываются внутри самого таймера, и нет необходимости в других межпоточных операциях.
Для событий типа Future.delayed
, имеющих таймер (_TIMEOUT_EVNET
), требуется использование EventHandler
для триггеринга операций epoll.
Процесс представлен ниже:
SendPort
и ReceivePort
._ZERO_EVENT
, после вставки в _enqueue
используется SendPort
для вызова обратного вызова._TIMEOUT_EVNET
, после вставки в _enqueue
через EventHandler
SendPort
передается в уровень C++ движка dart:io
, где с помощью механизма epoll реализуется триггеринг таймера через SendPort
.Здесь стоит отметить, что начиная с Dart 3.4 файл eventhandler_android.cc
был удалён, и вместо него используется eventhandler_linux.cc
, который использует epoll и timerfd для реализации таймеров.
epoll (eventpoll) — это механизм уведомлений о событиях ввода-вывода, уникальный для Linux, реализованный как модуль файловой системы в ядре Linux. timerfd представляет собой типовой файловый дескриптор таймера, создаваемый с помощью timerfd_create
, который активирует события при достижении времени.Если вас интересует запуск таймера, вы можете использовать входной метод DartVM::DartVM
в Flutter и вызвать BootstrapDartIo
как точку входа, начиная с EventHandler::Start
.
Кроме того, следует обратить внимание на то, что при выполнении _runnerTimers
каждый цикл for
вызывает _runPendingImmediateCallback
для выполнения очереди микротасков.
Почему scheduleMicrotask
имеет более высокий приоритет, чем Future
, можно понять из реализации таймера выше, так как каждый обратный вызов выполняет микротаск. Это также объясняет, почему Timer.periodic
не является "надёжным" периодическим действием, поскольку для виртуальной машины управление таймерами осуществляется в виде "пакетной обработки", которая должна учитывать возможность наличия микротасков. Об этом говорится в официальных комментариях.
Временная метка зависит от реализации внутреннего таймера. Не более
n
вызовов обратного вызова будет сделано за времяduration * n
, но время между двумя последовательными вызовами обратного вызова может быть как короче, так и длиннее чемduration
.
Я всегда чувствую, что использование слова "Timer" в контексте Dart асинхронной работы является лучшим примером понимания этого.
Если вы дошли до конца этой статьи, надеюсь, вы уже разобрались с этими вопросами.- Отношения между isolate, thread и runner
Надеюсь, что теперь вы имеете полное представление о реализации асинхронной работы в Flutter и Dart, и сможете проводить глубокий анализ этих тем во время собеседования на должность.
Конечно, в повседневной разработке вам, возможно, не потребуется знать все эти детали, поэтому эта часть материала обычно мало освещена и кажется "разрозненной". Для большинства людей это могут быть лишь теоретические знания, необходимые для прохождения собеседования.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )