DartSeniorTechnical

Как работает event loop в Dart?

Dart event loop: сначала исполняется весь synchronous код, затем microtask queue (Future.then, scheduleMicrotask) до конца, потом одно событие из event queue (таймеры, I/O). Каждый Isolate имеет собственный loop.

Как устроен event loop в Dart

Dart — однопоточный язык с двумя очередями: event queue (макрозадачи) и microtask queue (микрозадачи). Это похоже на браузерный event loop, но без Web API — всё асинхронное идёт через Future и Stream.

Порядок обработки на каждой итерации:

  1. Выполнить всю microtask queue до конца (включая микрозадачи, добавленные во время выполнения).
  2. Взять одно событие из event queue и выполнить его.
  3. Снова опустошить microtask queue.
  4. Повторить.

Microtask queue

Заполняется через scheduleMicrotask() или когда Future уже завершён в момент подписки. Callback .then() на завершённом Future попадает в microtask queue, а не в event queue.

import 'dart:async';

void main() {
  print('1: sync start');

  Future.value('done').then((v) {
    print('3: microtask — $v'); // Future уже готов → microtask
  });

  scheduleMicrotask(() => print('2: explicit microtask'));

  print('4: sync end');
}
// Вывод:
// 1: sync start
// 4: sync end
// 2: explicit microtask
// 3: microtask — done

Обратите внимание: синхронный код исполняется первым, потом — все микрозадачи в порядке добавления.

Event queue

Содержит I/O-события, таймеры, порты isolate. Future.delayed добавляет задачу в event queue после истечения таймера.

import 'dart:async';

void main() {
  Future.delayed(Duration.zero, () => print('B: event queue'));

  scheduleMicrotask(() => print('A: microtask'));

  print('sync');
}
// Вывод:
// sync
// A: microtask
// B: event queue

async/await под капотом

await — синтаксический сахар: приостанавливает выполнение функции и регистрирует продолжение как callback на Future. После await код выполняется в microtask queue (если Future уже завершён) или в event queue (когда I/O завершится).

Future<void> fetchData() async {
  print('before await');
  final result = await Future.delayed(
    const Duration(milliseconds: 100),
    () => 'data',
  );
  print('after await: $result'); // выполняется в event queue
}

void main() {
  fetchData();
  print('main continues'); // выполняется до 'after await'
}

Isolates и event loop

Каждый Isolate имеет собственный event loop и heap — изоляция памяти полная. Общение только через порты (SendPort/ReceivePort), сообщения сериализуются. Это принципиально отличается от worker threads в Node.js или Go goroutines.

import 'dart:isolate';

void heavyWork(SendPort sendPort) {
  // Выполняется в отдельном isolate, свой event loop
  final result = List.generate(1000000, (i) => i).fold(0, (a, b) => a + b);
  sendPort.send(result);
}

Future<int> runInIsolate() async {
  final receivePort = ReceivePort();
  await Isolate.spawn(heavyWork, receivePort.sendPort);
  return await receivePort.first as int;
}

Подводные камни

  • Бесконечная microtask queue: если microtask порождает другой microtask — event queue никогда не обработается, UI зависнет. В Flutter это приводит к дропу фреймов.
  • Блокировка event loop: синхронный CPU-интенсивный код (парсинг JSON, криптография) блокирует весь поток — нужен Isolate.run() или compute() во Flutter.
  • Future.delayed(Duration.zero) не гарантирует немедленность: задача попадёт в event queue и выполнится после всех текущих microtasks и, возможно, других event queue задач.
  • Порядок then()-callbacks: если несколько .then() навешаны на один Future — они выполнятся в порядке добавления, все как microtasks.
  • Утечка через порты isolate: незакрытый ReceivePort удерживает isolate живым — нужно явно вызывать receivePort.close().
  • async не означает параллельность: два await подряд — последовательные операции. Для параллелизма нужен Future.wait([f1, f2]) или Future.wait с Isolate.
  • StreamController без pause: если producer быстрее consumer и нет backpressure, буфер растёт бесконечно — используйте StreamController(sync: false) и слушайте onPause.
  • scheduleMicrotask в продакшене: прямое использование scheduleMicrotask редко оправдано — обычно достаточно Future.microtask() для явности намерения.

Common mistakes

  • Сводить «работает event loop в Dart» к синтаксису и не объяснять sound type system.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии dart-10.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «работает event loop в Dart» и подтверждает ее корректным примером.
  • Умеет связать ответ с event loop, тестированием и отладкой на устройстве.
  • Называет ограничения подхода dart-10, включая производительность, память и сопровождение.

Sources

Related topics