Как устроена асинхронность (async/await) в Dart?

И так, я изучаю Dart и добрался до темы async/await, но не понимаю как она устроена. Далеко ходить не буду, а сразу пример:

void main() {
  print("${DateTime.now()} main");
  func();
  func3();
}

func() async {
  var result = await func2();
  print("${DateTime.now()} $result");
}

Future<String> func2() async {
  // await Future.delayed(Duration.zero);
  for (int i = 0; i < 10000000000; ++i) {}
  return "func2";
}

func3() {
  print("${DateTime.now()} func3");
}

Результат выполнения данного кода будет следующим:

2024-02-20 11:20:07.035210 main
2024-02-20 11:20:09.484226 func3
2024-02-20 11:20:09.485226 func2

Вопрос: почему лог func3 выводится одновременно с func2? Я так понимаю, это из-за того, что функция считается асинхронной, если в ней есть await или возврат Future, потому что если я уберу комментарий со строки await Future.delayed(Duration.zero); (которая по сути бесполезна) или сделаю нечто такое:

Future<String> func2() async {
  return Future<String>(() {
    for (int i = 0; i < 10000000000; ++i) {}
    return "func2";
  });
}

, то посмотрите как меняется вывод:

2024-02-20 11:23:30.396179 main
2024-02-20 11:23:30.400179 func3
2024-02-20 11:23:32.910888 func2

Теперь, лог func3 не выводится вместе с func2 — это я и понимаю под асинхронностью. Выполнение программы в main() идёт дальше, а не останавливается на func(). Почему для этого обязательно наличие await и использования Future в любом виде? Почему как только я добавляю Future, то асинхронность сразу же работает? Также кстати и с Stream. Компилятор вообще никак не реагирует на то, что я пометил функцию как async?

P.S. Вот допустим пример кода на Java на котором я делаю практически тоже самое — запускаю отдельный поток и иду дальше. Фраза "main" будет выведена моментально, а остальной код будет выполнятся параллельно сразу же. То есть, он начнет работу не в момент вызова future.get(), а сразу же. От Dart ожидаю тоже самое, но работает как то по-другому... (да, тут код в потоке зависнет из-за переполнения типа)

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            for (int i = 0; i < Integer.MAX_VALUE; ++i) {
                i += 1;
            }
            return "Hello";
        });
        System.out.println("main");
        System.out.println(future.get());
    }

Ответы (1 шт):

Автор решения: MiT

TL;DR;

Разберем пример кода и работу async/await в Dart, объясняя, почему получается именно такой вывод.

Ключевые моменты для понимания асинхронности в Dart:

  • Ключевое слово async указывает, что функция может работать асинхронно и возвращает Future. Даже если функция не содержит await, она неявно возвращает Future.
  • Ключевое слово await используется внутри async функции и приостанавливает выполнение функции до тех пор, пока Future (результат работы другой асинхронной операции) не завершится. Важно: await не блокирует основной поток (isolate) Dart.
  • Future представляет результат асинхронной операции. Он может находиться в одном из трех состояний:
    • Незавершенный (uncompleted): операция еще выполняется.
    • Завершенный успешно (completed): операция завершена, и Future содержит результат.
    • Завершенный с ошибкой (completed with error): операция завершена с ошибкой.
  • Event Loop (Цикл событий): Dart использует цикл событий для обработки асинхронных операций. Когда встречается await, текущая функция приостанавливается, и управление возвращается в цикл событий. Цикл событий продолжает обрабатывать другие задачи. Когда Future после await завершается, цикл событий возобновляет выполнение приостановленной функции с того места, где она остановилась.

Теперь разберем пошагово выполнение вашего кода:

Давайте разберем как работает асинхронность в вашем примере:

  1. Сначала выполняется main() и печатается первая строка с временем.
  2. Затем вызывается func(). Поскольку она помечена как async, она возвращает Future. При этом:
    • Когда встречается await, выполнение func() приостанавливается
    • Управление возвращается в main()
    • Dart запоминает, что нужно вернуться к выполнению func() после завершения func2()
  3. main() продолжает выполнение и вызывает func3(), которая сразу печатает своё сообщение.
  4. Параллельно выполняется func2(). Хотя она помечена как async, в ней нет реальной асинхронности - только тяжелый синхронный цикл. Поэтому она блокирует выполнение.
  5. Когда func2() завершается, управление возвращается в func() на строку после await, и печатается результат.

Тут нужно рассказать ещё подробно об Event Loop.

Event Loop содержит несколько очередей:

  • Microtask queue (микрозадачи) - очередь с наивысшим приоритетом
  • Event queue (события) - основная очередь для асинхронных операций

Порядок обработки:

while (true) {
  // 1. Выполняет микрозадачи
  while (microtaskQueue.isNotEmpty) {
    var task = microtaskQueue.removeFirst();
    execute(task);
  }
  
  // 2. Выполняет события
  if (eventQueue.isNotEmpty) {
    var event = eventQueue.removeFirst();
    execute(event);
  }
}
  1. Пример порядка выполнения:
void main() {
  print('start'); // 1
  
  Future(() => print('event queue')); // 4
  
  Future.microtask(() => print('microtask queue')); // 2
  
  Future.delayed(
    Duration(seconds: 1),
    () => print('timer queue'), // 5
  );
  
  print('end'); // 3
}

Важные особенности:

  • Event Loop начинает работу только после завершения синхронного кода
  • Микрозадачи всегда выполняются до событий
  • Новые микрозадачи, добавленные во время выполнения текущей микрозадачи, будут выполнены до перехода к событиям
  • Таймеры выполняются только когда подойдет их время
  • Event Loop работает в одном потоке

Изоляты (Isolates):

  • Каждый изолят имеет свой собственный Event Loop
  • Изоляты работают параллельно и не делят память
  • Общаются через передачу сообщений

Если обе очереди в Event Loop (и очередь микрозадач, и очередь событий) пусты, Dart runtime (среда выполнения) переходит в состояние ожидания. Он перестает активно выполнять код и ждет появления новых событий.

Что происходит дальше, зависит от конкретной платформы и реализации Dart runtime:

  • Нативные платформы (мобильные, десктоп): Dart runtime обычно взаимодействует с операционной системой, которая уведомляет его о наступлении новых событий (например, завершение I/O-операции, ввод пользователя, таймер). Runtime "спит", потребляя минимальные ресурсы, пока не получит такое уведомление. Получив уведомление, runtime "просыпается", добавляет соответствующее событие в очередь событий и возобновляет работу Event Loop.
  • Веб (Dart компилируется в JavaScript): В браузере Dart runtime интегрируется с JavaScript Event Loop. Когда очереди Dart Event Loop пусты, управление возвращается в JavaScript Event Loop. Браузер обрабатывает свои собственные события (например, отрисовка страницы, обработка событий DOM). Когда происходит событие, которое должно быть обработано Dart кодом (например, завершение XMLHttpRequest), браузер добавляет соответствующее событие в очередь событий Dart, и Dart Event Loop возобновляет работу.

Ну и под конец, это тоже важно, программа на Dart завершается, когда выполняются все следующие условия:

  • Выполнен весь синхронный код
  • Все очереди Event Loop пусты (microtask queue, event queue)
  • Нет активных таймеров
  • Нет незавершенных операций ввода/вывода
  • Нет других источников событий
→ Ссылка