24.09.2026
Асинхронность в Dart: Event Loop, Future и Stream для мидл-разработчика
Dart — однопоточный язык с асинхронной моделью выполнения, построенной на event loop. Понимание этой модели отличает мидл-разработчика от джуниора: джуниор просто пишет await и надеется, что всё сработает, а мидл понимает, почему код выполняется именно в таком порядке, и умеет предсказывать поведение программы под нагрузкой.
Однопоточность и изоляты
Главный тезис, который нужно усвоить: Dart-код в рамках одного изолята выполняется в одном потоке. Это означает, что никакие две функции не выполняются одновременно. Параллелизм достигается не потоками, а изолятами (isolates), каждый из которых имеет собственную память и собственный event loop.
Внутри одного изолята concurrency реализуется через очередь событий. Задача разработчика — не блокировать этот поток долгими синхронными операциями, иначе UI зависнет или сервер перестанет отвечать на другие запросы.
Event Loop: две очереди
Event loop в Dart обрабатывает две очереди в строго определённом порядке:
Microtask queue — очередь микрозадач, обрабатывается до полного опустошения.
Event queue — очередь событий: таймеры, I/O, сообщения из портов, колбэки Future.
Алгоритм работы цикла:
Выполнить весь синхронный код текущего стека.
Опустошить microtask queue полностью, включая микрозадачи, добавленные в процессе выполнения.
Взять одно событие из event queue и выполнить его.
Снова опустошить microtask queue.
Повторять шаги 3–4 бесконечно.
Отсюда практическое следствие: микрозадачи всегда имеют приоритет над событиями. Если бесконечно добавлять микрозадачи, event queue никогда не будет обработана — поток зависнет.
Порядок выполнения на примере
Рассмотрим классический пример, который часто спрашивают на собеседовании:
print('A') — синхронный код, выполняется первым.
Future(() => print('B')) — планирует событие в event queue.
Future.microtask(() => print('C')) — планирует микрозадачу.
print('D') — синхронный код, выполняется до цикла.
Порядок вывода: A, D, C, B. Сначала весь синхронный код, затем microtask, и только потом event.
Future: контракт и распространённые ошибки
Future<T> — это объект, представляющий результат асинхронной операции, который станет доступен позже. У него три состояния: uncompleted, completed with value, completed with error.
Ключевые моменты, которые мидл обязан знать:
await не блокирует поток — он приостанавливает выполнение текущей функции и возвращает управление event loop.
Ошибка внутри async-функции автоматически оборачивается в Future.error.
Необработанная ошибка в Future приводит к unhandled exception и может уронить изолят.
Future.wait завершается с ошибкой при первом же отклонённом Future, если не указан параметр eagerError: false.
Пример корректной обработки
Плохой вариант — ловить ошибку через try/catch вокруг await, но забывать про случаи, когда Future не ожидается. Хороший вариант — явно обрабатывать ошибку на границе:
Использовать try/catch вокруг await для синхронного стиля.
Использовать .catchError для цепочек без await.
Никогда не игнорировать возвращаемый Future без обработки ошибок.
Stream: поток данных во времени
Stream<T> — это последовательность асинхронных событий. Если Future — это одно значение в будущем, то Stream — это много значений, распределённых во времени. Различают два вида:
Single-subscription stream — допускает только одного слушателя, события буферизуются до подписки.
Broadcast stream — допускает множество слушателей, события теряются, если никто не слушает.
Мидл должен понимать разницу между StreamController и StreamController.broadcast, а также уметь трансформировать потоки через map, where, asyncMap, transform.
Backpressure и управление потоком
Одна из частых проблем при работе со Stream — backpressure, когда производитель генерирует события быстрее, чем потребитель успевает их обрабатывать. Решения:
Использовать await for, который естественным образом приостанавливает обработку.
Применять pause() и resume() на подписке.
Ограничивать буфер через StreamController с явной стратегией.
Zone и перехват асинхронных ошибок
Zone — механизм, позволяющий перехватывать и обрабатывать асинхронные ошибки и управлять контекстом выполнения. Фреймворки вроде Flutter используют зоны для перехвата ошибок в кадре рендеринга. Мидл должен знать, что runZonedGuarded позволяет обернуть всё приложение и логировать любые необработанные асинхронные исключения.
Практические выводы
Не блокируйте event loop синхронными вычислениями — выносите их в изолят через compute или Isolate.run.
Помните о приоритете microtask queue: не злоупотребляйте scheduleMicrotask.
Всегда обрабатывайте ошибки в Future и Stream — иначе получите молчаливый сбой в продакшене.
Различайте single-subscription и broadcast streams и выбирайте подходящий тип осознанно.
Используйте Zone для централизованного логирования асинхронных ошибок.
Понимание event loop — это фундамент, на котором строятся в