1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-N45.md 25 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Введение в асинхронное выполнение Dart в Flutter

Почему была создана эта статья? Из-за того что ранее были поступили некоторые «вопросы» относительно «Интересных асинхронных вопросов в интервью», но объяснить эти вопросы за несколько слов было затруднительно, поэтому было решено написать полное объяснение. Надеюсь, данная статья поможет вам полностью понять реализацию асинхронной работы Dart в Flutter.

⚠️ Данная статья может содержать большое количество информации, основана на Flutter 3.22+ и Dart 3.4+, пожалуйста, будьте терпеливы при чтении.

Очереди (Isolates)

Если вы хотите объяснить механизм асинхронной работы в Dart, то обязательно придется упомянуть очереди (isolates). Для полного понимания асинхронной работы важно начинать с изучения очередей, так как каждый Dart-код выполняется внутри определённого isolate, например, наш входной метод main выполняется внутри корневого isolate, который можно рассматривать как главный поток Dart-кода. Код Dart в одном isolate работает в однопоточной модели, используя цикл событий для реализации асинхронных операций. С точки зрения разработчика, каждый isolate можно рассматривать как отдельный поток, хотя строго говоря это неверно.

Isolates и группы isolates

image1.png

Рассказывая об isolates, нельзя обойтись без этой старой диаграммы. Давайте рассмотрим некоторые базовые концепции:- Каждый isolate имеет своё глобальное состояние (global state), рабочий поток (mutator thread) и вспомогательный поток (helper thread).

  • Isolates группируются в группы isolates, одна группа делится одним управляемым кучей сборщиком мусора (GC-managed heap), используемым для хранения объектов, распределённых изолятами. Здесь стоит отметить, что все изоляты одной группы используют общую кучу.

Как объекты, реализующие параллелизм, isolates, как следует из их названия, представляют собой "изоляцию". Они работают независимо друг от друга, не могут использовать общую память и взаимодействуют между собой только через порты (ports).

Группы isolates были введены в Dart 2.15. Isolates в группах isolates совместно используют различные внутренние структуры данных текущего запущенного приложения.

После Dart 2.15 запуск дополнительных isolates в группах isolates стал быстрее примерно в 100 раз, поскольку теперь нет необходимости инициализировать структуры программы, а также объём необходимой памяти для создания новых isolates уменьшился в 10-100 раз.

Метод Isolate.spawn позволяет создать новый isolate в рамках одной группы, а Isolate.spawnUri — запустить новую группу. Хотя группы изолируемых объектов всё ещё не позволяют изоляциям делиться изменяемыми объектами, внутри группы можно использовать общую область памяти, что позволяет раскрыть больше возможностей, например, передача объекта из одного изолята в другой, а не только базовых типов.Итак, основные концепции изолятов и групп изолятов должны быть поняты, теперь давайте обсудим взаимоотношения между изолятом и внешним миром.

Изолят и поток

Мы знаем, что изоляты могут выполнять операции с потоками, но они всё ещё зависят от системных потоков для выполнения задач. Тогда вопрос: есть ли одно-к-одному соотношение между системными потоками и изолятами? Ответ — нет.

Простое объяснение:

  • Один системный поток может одновременно работать только с одним изолятом; если он хочет перейти к другому изоляту, ему нужно покинуть текущий изолят.
  • Один системный поток всегда связан только с одним мутатором изолята; мутаторный поток — это тот поток, который выполняет код Dart в изоляте.

Несколько менее очевидных отношений:

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

Поэтому изоляты и системные потоки точно не имеют отношения одно-к-одному; фактически, внутренняя виртуальная машина Dart использует пулы потоков (ThreadPools) для управления системными потоками, а изоляты не являются постоянными "мертвыми" циклами на потоках, и код Dart VM реализован вокруг логики ThreadPool::Task, а не системных потоков.> Например, когда изолят обрабатывает цикл событий, он отправляет MessageHandlerTask в пулы потоков, после чего выбирается свободный поток или создаётся новый поток для выполнения этой задачи. После завершения работы поток может продолжить работу над другими задачами изолята, в зависимости от конкретной ситуации.

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

На следующих диаграммах показана приближенная связь между изолятами и потоками, хотя она и не является строго научной, но достаточно простой для понимания.

image

image

Изолят и запускатор

Концепция запускатора может показаться незнакомой, но, например, в Xcode имя проекта по умолчанию часто называется Runner. Однако запускатор является абстрактной концепцией в Flutter и не имеет прямого отношения к изолятам.

Для Flutter Engine он может отправлять задачи в запускатор, поэтому его также называют TaskRunner. Например, в Flutter есть четыре TaskRunner (UI, GPU, IO, Platform).

imageДля Flutter Engine важно знать, что он не заботится о том, какой именно поток используется для выполнения TaskRunner. Однако самому TaskRunner лучше всегда работать в одном и том же потоке. Например, Android и iOS создают отдельные потоки для UI, GPU и IO. В частности, UI TaskRunner представляет собой Dart root isolate, то есть основной поток Dart.> Что касается Platform Runner, это главный поток платформы устройства. На мобильных платформах этот поток является общим, и все экземпляры Engine используют один и тот же Platform Runner и поток.

Поэтому в Flutter:

  • Задачи в Engine отправляются в Runner, где они выполняются потоком, содержащим Runner.
  • Isolates управляются Dart VM, а Flutter Engine не обращается напрямую к ним.

Хотя Runner и isolates не имеют прямого отношения, между ними всё равно существует "взаимодействие", например, между root isolate и UI Runner.

По нашему мнению, UI Runner представляет собой UI поток Dart в Flutter, а root isolate — это основной поток Dart-кода. Таким образом, очевидно, что UI Runner и root isolate находятся в одном и том же потоке.

Чтобы узнать больше об этом выводе, можно рассмотреть процесс запуска Flutter Engine, как показано ниже. Это полный процесс создания root isolate, ключевой момент которого заключается в методе SetMessageHandlingTaskRunner.

image

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 взаимодействуют через механизм распределения задач, например:

  • root isolate использует вызовы Dart для обращения к C++ для передачи задач, связанных с рендерингом UI, для выполнения в UI Runner;
  • UI Runner также может обратиться к isolate через события.

Таким образом, отношения между 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 и Event заключается в том, что MicroTask представляет собой минимальную единицу работы, которая должна быть выполнена асинхронно, но не запущена внешними событиями. Это станет очевидным после сравнения с Future. Что касается отношения между микротасками и очередями событий, давайте рассмотрим пример с приведенным ниже кодом:

test() async {
    print('начинается');
    Future(() => print('Это новый Future'));
    Future.microtask(() => print('Это микротаск'));
    print('завершается');
}

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

Почему мы не используем микротаски чаще, если они имеют более высокий приоритет? Это связано с тем, что Flutter "настраивает" микротаски таким образом, чтобы они использовали ресурсы движка.

Диаграмма

Кроме того, в движке Flutter есть несколько хитростей относительно выполнения микротасков:

  • Во время BeginFrame выполняются микротаски;
  • При создании root isolate создается объект 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.

Процесс представлен ниже:

    1. При создании таймера создаётся SendPort и ReceivePort.
    1. Для операций типа _ZERO_EVENT, после вставки в _enqueue используется SendPort для вызова обратного вызова.
    1. Для операций типа _TIMEOUT_EVNET, после вставки в _enqueue через EventHandler SendPort передается в уровень C++ движка dart:io, где с помощью механизма epoll реализуется триггеринг таймера через SendPort.

01

Здесь стоит отметить, что начиная с 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

  • Принципы и различия между microtask и event
  • Почему microtask выполняется раньше
  • Различие и реализация между Future() и Future.microtask()
  • Реализация и логика timer

Надеюсь, что теперь вы имеете полное представление о реализации асинхронной работы в Flutter и Dart, и сможете проводить глубокий анализ этих тем во время собеседования на должность.

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

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master