> Как устроена многопоточность в Dart (Flutter)

Уровень: senior · Роль: frontend · Категория: Технические вопросы

Компании: Верме

Стек: Flutter

> Пример ответа

Короткий ответ

В Dart многопоточности в классическом понимании нет - используется изоляты (isolates), каждый со своей памятью и event loop. Основной поток - main isolate, для фоновой работы создаются дополнительные через Isolate.spawn или пул через compute во Flutter. Обмен данными - через порты и сообщения, либо через SendPort/ReceivePort. Также есть Isolate.run для простых задач. Внутри изолята код выполняется в одном потоке, но асинхронность достигается через event loop и Future/Stream.

Подробное объяснение

Модель конкурентности в Dart построена на изолятах, а не на разделяемой памяти и потоках, как в Java или C++. Каждый изолят - это отдельный исполняющийся контекст со своим heap и event loop. Между изолятами нет общего состояния, поэтому гонки данных и блокировки в классическом виде отсутствуют.

Event loop внутри изолята обрабатывает микротаски (microtasks) и события (events). Микротаски - это Future и async/await, события - таймеры, I/O, сообщения от других изолятов. Это позволяет писать асинхронный код без блокировки основного потока.

Для создания изолята используется Isolate.spawn(entryPoint, message). Точка входа - топ-левел функция или статический метод. Передача данных - через SendPort, который упаковывает сообщение в копию (deep copy) для большинства типов. Исключение - TransferableTypedData для эффективной передачи больших бинарных данных.

Во Flutter для фоновых задач удобно использовать compute - обёртку над Isolate.spawn, которая принимает функцию и аргумент, возвращает Future. Она подходит для CPU-bound задач, но не для длительных операций с прогрессом - для них нужен полноценный изолят с портами.

Важный нюанс: изоляты не разделяют память, поэтому нет атомарных операций и мьютексов. Если нужно общее состояние - используют IsolateNameServer или внешние хранилища (например, SharedPreferences через платформенные каналы).

На практике

В Flutter основной изолят отвечает за UI и обработку жестов. Любая тяжёлая синхронная операция в нём блокирует рендеринг - это видно как "jank" и потеря кадров. Поэтому типичный паттерн: вынести парсинг JSON, шифрование, обработку изображений, сложные вычисления в отдельный изолят.

Для простых задач - compute. Для более сложных сценариев (прогресс, отмена, поток данных) - ручное создание изолята с ReceivePort и отправкой сообщений. Также есть Isolate.run - удобный способ запустить функцию в новом изоляте и получить результат, но без возможности общаться в процессе выполнения.

Стоит помнить про ограничения: передача больших объектов через SendPort копирует данные, что может быть дорого. Для больших бинарных данных используют TransferableTypedData. Также изоляты не дешёвые - создание занимает время и память, поэтому не стоит создавать их на каждую мелкую операцию.

Пример кода

import 'dart:isolate';
import 'dart:typed_data';

// Простой случай через compute (Flutter)
Future<int> heavyComputation(int value) async {
  return await compute(_expensiveFunction, value);
}

int _expensiveFunction(int value) {
  var result = 0;
  for (var i = 0; i < value; i++) {
    result += i;
  }
  return result;
}

// Ручное создание изолята с обменом сообщениями
Future<void> spawnIsolate() async {
  final receivePort = ReceivePort();
  final isolate = await Isolate.spawn(_isolateEntry, receivePort.sendPort);
  
  receivePort.listen((message) {
    if (message is String) {
      print('Received: $message');
      isolate.kill(priority: Isolate.immediate);
    }
  });
}

void _isolateEntry(SendPort sendPort) {
  sendPort.send('Hello from isolate');
}

// Передача больших данных через TransferableTypedData
Future<void> transferLargeData() async {
  final receivePort = ReceivePort();
  final isolate = await Isolate.spawn(_transferEntry, receivePort.sendPort);
  
  final data = Uint8List.fromList(List.generate(1000000, (i) => i % 256));
  final transferable = TransferableTypedData.fromList([data]);
  
  receivePort.listen((message) {
    if (message is String) {
      print(message);
      isolate.kill(priority: Isolate.immediate);
    }
  });
  
  final sendPort = await receivePort.first;
  sendPort.send(transferable);
}

void _transferEntry(SendPort sendPort) {
  final receivePort = ReceivePort();
  sendPort.send(receivePort.sendPort);
  
  receivePort.listen((message) {
    if (message is TransferableTypedData) {
      final data = message.materialize().asUint8List();
      print('Got ${data.length} bytes');
    }
  });
}

Как отвечать на собеседовании

Начни с ключевого отличия: в Dart нет потоков с общей памятью, есть изоляты. Подчеркни, что это архитектурное решение для избежания гонок и блокировок. Затем опиши event loop и разницу между микротасками и событиями - это покажет понимание асинхронности.

Расскажи про способы создания изолятов: Isolate.spawn, Isolate.run, compute во Flutter. Упомяни ограничения: копирование данных при передаче, стоимость создания, невозможность разделять состояние. Если спросят про производительность - объясни, что изоляты полезны для CPU-bound задач, но для I/O часто достаточно асинхронности в одном изоляте.

Приведи пример из практики: парсинг большого JSON или обработка изображения в фоне. Покажи, как выбираешь между compute и ручным изолятом. Если спросят про отмену - упомяни Isolate.kill и паттерны с портами.

Что проверяет интервьюер

Интервьюер оценивает понимание модели конкурентности в Dart, а не просто знание API. Важно показать, что ты понимаешь, почему изоляты - это не потоки, и какие последствия это имеет для дизайна приложения. Проверяется умение объяснить event loop, разницу между микротасками и событиями, ограничения передачи данных.

Также смотрят на практический опыт: когда использовать compute, а когда полноценный изолят, как обрабатывать ошибки в изолятах, как управлять жизненным циклом. Хороший ответ включает trade-off: изоляты дороже потоков, но безопаснее. Если кандидат упоминает IsolateNameServer или TransferableTypedData - это плюс, значит, сталкивался с реальными задачами.

Типичные ошибки

Главная ошибка - называть изоляты "потоками" и пытаться использовать мьютексы или атомарные операции. Это показывает непонимание модели. Вторая ошибка - думать, что async/await создаёт новый поток: это не так, код выполняется в том же изоляте.

Часто забывают, что передача объектов через SendPort - это копирование, и для больших данных это дорого. Также ошибочно создавать изолят на каждую мелкую операцию - это медленно. Ещё одна ошибка - не обрабатывать ошибки в изоляте: исключение в фоновом изоляте не пробрасывается в основной автоматически, нужно использовать ReceivePort и передавать ошибку явно.

Некоторые путают compute с полноценным изолятом и пытаются использовать его для длительных задач с прогрессом - это не работает, потому что compute возвращает один результат и не поддерживает промежуточные сообщения.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью