Код Dart выполняется в изоляте: у него своя память и один поток. Внутри изолята работает event loop с двумя очередями. Понимание их порядка нужно и для задач «что выведет», и для отладки зависаний интерфейса.
Две очереди
- Microtask queue:
scheduleMicrotask,Future.microtask, колбэкиthenу уже завершённого Future, продолжение послеawait. - Event queue: таймеры (
Future(...),Future.delayed,Timer), ввод, ответы ввода-вывода, сообщения от других изолятов.
Цикл: выполнить синхронный код до конца, затем все микрозадачи, затем одно событие, снова все микрозадачи, и так далее.
void main() {
print('1');
Future(() => print('2'));
scheduleMicrotask(() => print('3'));
Future.value(4).then(print);
print('5');
}
// 1 5 3 4 2
Кадры Flutter тоже приходят через event loop. Пока работает долгий синхронный код или бесконечная цепочка микрозадач, кадр не отрисуется, и интерфейс подвиснет.
Future и async/await
Future<T> это значение, которое появится позже, или ошибка. async-функция всегда возвращает Future и выполняется синхронно до первого await. После await продолжение планируется асинхронно, даже если ожидаемое значение уже готово.
Future<void> load() async {
print('A');
await null; // всё равно отдаёт управление
print('B');
}
void main() {
load();
print('C'); // A C B
}
Ошибки в async-функции превращаются в завершённый ошибкой Future. Ловятся через try/catch вокруг await или catchError. Future без await и без обработчика ошибки даст необработанную ошибку зоны. Если результат намеренно не ждём, пишут unawaited(f()), а линтер unawaited_futures напоминает о забытых await.
Параллельно: await Future.wait([a(), b()]) запускает оба сразу. Последовательное await a(); await b(); в два раза дольше, если задачи независимы. С Dart 3 можно ждать record: final (u, p) = await (getUser(), getPosts()).wait;.
Stream
Stream это последовательность асинхронных событий: данные, ошибки и завершение.
- Single-subscription (по умолчанию): один слушатель, события буферизуются, пока слушателя нет. Пример: чтение файла, HTTP-ответ.
- Broadcast: много слушателей, события без слушателя теряются. Пример: события кликов, шина событий.
final sub = stream.listen(onData, onError: onError, onDone: onDone);
await sub.cancel(); // обязательно в dispose
async* с yield создаёт Stream, await for его читает. StreamController даёт ручное управление: add, addError, close. Незакрытые контроллеры и неотменённые подписки дают утечки памяти и вызовы setState после dispose.
Изоляты
Изоляты не делят память, общаются сообщениями через порты. Поэтому в Dart нет гонок данных на общих объектах, но передача больших данных стоит копирования.
final result = await Isolate.run(() => parseHugeJson(text));
// во Flutter то же самое: compute(parseHugeJson, text)
Когда нужен изолят: тяжёлые вычисления больше нескольких миллисекунд, парсинг большого JSON, обработка изображений. Сетевые запросы и чтение файлов в изолят выносить не нужно: это ожидание, а не работа CPU, оно и так не блокирует поток.
Для долгоживущего фонового воркера используют Isolate.spawn и пару ReceivePort/SendPort. В фоновом изоляте можно звать платформенные каналы после BackgroundIsolateBinaryMessenger.ensureInitialized.
Частые ошибки
- Думать, что
asyncсразу уводит выполнение в фон: код до первогоawaitсинхронный. - Тяжёлый цикл в
async-функции: он всё равно блокирует UI. - Забытый
awaitи молча проглоченная ошибка. listenбезcancelвdispose.
Что спросят на собесе
- Порядок microtask и event queue, задачи на вывод.
- Чем Future отличается от Stream, single-subscription от broadcast.
- Как распараллелить независимые запросы.
- Что такое изолят, чем отличается от потока, когда нужен
compute. - Почему
asyncне спасает от фризов при тяжёлых вычислениях.