> Как устроена многопоточность в 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 возвращает один результат и не поддерживает промежуточные сообщения.
> Похожие задачи по frontend
Что такое типы данных в Dart
Почему желательно не использовать KISS и DRY в билде Flutter
Что такое миксины в Dart
Что такое дерево виджетов во Flutter
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью