Как устроена асинхронность (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 шт):
Разберем пример кода и работу 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завершается, цикл событий возобновляет выполнение приостановленной функции с того места, где она остановилась.
Теперь разберем пошагово выполнение вашего кода:
Давайте разберем как работает асинхронность в вашем примере:
- Сначала выполняется
main()и печатается первая строка с временем. - Затем вызывается
func(). Поскольку она помечена какasync, она возвращает Future. При этом:- Когда встречается
await, выполнениеfunc()приостанавливается - Управление возвращается в
main() - Dart запоминает, что нужно вернуться к выполнению
func()после завершенияfunc2()
- Когда встречается
main()продолжает выполнение и вызываетfunc3(), которая сразу печатает своё сообщение.- Параллельно выполняется
func2(). Хотя она помечена какasync, в ней нет реальной асинхронности - только тяжелый синхронный цикл. Поэтому она блокирует выполнение. - Когда
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);
}
}
- Пример порядка выполнения:
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)
- Нет активных таймеров
- Нет незавершенных операций ввода/вывода
- Нет других источников событий