Как устроена многопоточность в Dart
Что проверяют
Понимаете ли вы, что async и await в Dart не создают потоков, и знаете ли, когда нужен изолят. Спрашивают почти всегда на примере интерфейса: почему лагает экран при разборе большого JSON. Следом уточняют, как изоляты обмениваются данными.
Как отвечать
Начните с того, что общей памяти между потоками в Dart нет. Единица параллельности это изолят: своя куча, свой цикл событий, один поток исполнения.
Внутри изолята работает event loop с двумя очередями:
- microtask queue:
scheduleMicrotaskи обработчики завершённыхFuture, очередь выбирается целиком перед следующим событием; - event queue: таймеры, ввод-вывод, события интерфейса, сообщения от других изолятов.
Future и async/await дают конкурентность на одном потоке: пока ждём сеть, цикл обрабатывает другие события. Но тяжёлое вычисление в async-функции всё равно блокирует изолят, а при 60 Гц кадр, не уложившийся примерно в 16 мс, пропадает.
Для CPU-работы нужен второй изолят:
Isolate.run(): появился в Dart 2.19, запускает функцию в новом изоляте и возвращает результат какFuture;compute()во Flutter: удобная обёртка с той же идеей;Isolate.spawn()плюсSendPortиReceivePort: долгоживущий воркер с обменом сообщениями.
Сообщения между изолятами копируются. Результат, отданный через Isolate.exit(), передаётся без копирования, а для больших буферов есть TransferableTypedData.
Важная деталь для веба: dart:isolate в браузерной сборке недоступен, а compute() там выполняет функцию в том же потоке. Настоящий параллелизм в браузере даёт только Web Worker.
Где спотыкаются
Говорят, что Future запускает код в отдельном потоке. После этого вопрос «почему тогда интерфейс фризит» остаётся без ответа.
Отправляют в изолят замыкание, которое захватило большой объект или несериализуемый ресурс вроде сокета, и получают ошибку либо дорогое копирование. Передавайте простые данные, а замыкание держите маленьким.
Создают изолят на каждую мелочь. Запуск и копирование данных стоят дороже, чем выигрыш на задаче в пару миллисекунд.