Irbisa · Шпаргалка

Изоляты и настоящая многопоточность

Isolate.run, spawn, порты, копирование сообщений, пулы воркеров, TransferableTypedData

Senior-разработчик · 18 вопросов

  1. 01

    Как ReceivePort и SendPort позволяют двум изолятам общаться и почему это безопаснее потоков с общей памятью?

    У каждого изолята своя куча и свой event loop, поэтому ни один объект никогда не разделяется как изменяемый; два изолята общаются только скопированными сообщениями через порты.

  2. 02

    Flutter однопоточный? Какие потоки есть у Flutter-приложения и какой из них выполняет ваш Dart-код?

    Не совсем: движок использует несколько потоков, но ваш Dart-код выполняется ровно в одном, а именно в корневом изоляте.

  3. 03

    В чём разница между compute(), Isolate.run() и Isolate.spawn() и когда нужен именно spawn?

    compute и Isolate.run работают как одноразовые помощники: запустить воркер, выполнить одну функцию, вернуть один результат, завершиться; на нативных платформах compute во Flutter просто вызывает Isolate.run.

    // one argument in, one value outFuture<List<Order>> parse(String body) =>    compute(parseOrders, body, debugLabel: 'parseOrders'); // a closure: captures several inputs, and may be asyncFuture<Report> buildReport(List<OrderRow> rows, Filter filter) =>…
  4. 04

    Найдите баг: индикатор загрузки замирает на полсекунды, когда приходит отчёт, хотя load объявлен async.

    class ReportRepository {  ReportRepository(this._api);  final ReportApi _api;   Future<Report> load() async {    final body = await _api.fetchReport(); // ~8 MB of JSON    return _parse(body);  }   Future<Report> _parse(String body) async {    final json = await Future(() => jsonDecode(body) as Map<String, dynamic>);    return Report.fromJson(json);  }}

    Future(() => ...) ничего не уносит с UI-изолята: он лишь ставит колбэк в очередь того же event loop, поэтому jsonDecode и fromJson всё так же выполняются там одним длинным событием, и каждый кадр ждёт их окончания.

  5. 05

    Что выведет этот код и почему воркер не видит значение, которое выставил главный изолят?

    import 'dart:isolate'; int counter = 0; Future<void> main() async {  counter = 7;  final fromWorker = await Isolate.run(() {    counter += 100;    return counter;  });  print(fromWorker);  print(counter);}

    Он выведет 100, а затем 7.

  6. 06

    Вызов Isolate.run внутри State падает с «object is unsendable»: что он на самом деле пытается отправить?

    class _ReportPageState extends State<ReportPage> {  final _scroll = ScrollController();  String _raw = '';  List<OrderRow> _rows = const [];   Future<void> _parse() async {    final rows = await Isolate.run(() => parseRows(_raw));    setState(() => _rows = rows);  }   // build() omitted}

    Весь State.

  7. 07

    Этот handshake с воркером падает с «Bad state: Stream has already been listened to». Что не так и как должен выглядеть handshake?

    Future<SendPort> startWorker() async {  final fromWorker = ReceivePort();  await Isolate.spawn(workerMain, fromWorker.sendPort);   final toWorker = await fromWorker.first as SendPort;  fromWorker.listen((reply) => handleReply(reply));  return toWorker;} void workerMain(SendPort toMain) {  final inbox = ReceivePort();  toMain.send(inbox.sendPort);  inbox.listen((job) => toMain.send(process(job)));}

    ReceivePort является стримом с одной подпиской: first подписывается, берёт одно сообщение и отписывается, а это ещё и закрывает порт, поэтому второй listen бросает ошибку.

  8. 08

    Исключение внутри запущенного изолята не доходит до вашего try/catch, а экран крутит лоадер вечно: куда делась ошибка?

    Она убила воркер и напечаталась в консоль.

    Future<Isolate> startImporter(SendPort results, void Function() failAll) {  final errors = ReceivePort();  final exits = ReceivePort();  errors.listen((pair) {    final [error, stack] = pair as List; // two Strings, never the object    reporter.record(error as String, StackTrace.fromString(stack as String? ?? ''));…
  9. 09

    Вы вызываете isolate.kill() для воркера, застрявшего в двухсекундном цикле, а он всё равно доводит цикл до конца: что на самом деле обещает kill?

    kill() по умолчанию использует priority: Isolate.beforeNextEvent: остановиться, когда управление в следующий раз вернётся в event loop.

    void spinner(SendPort toMain) {  final sw = Stopwatch()..start();  while (sw.elapsedMilliseconds < 2000) {} // no await: one single event  toMain.send('finished');}…
  10. 10

    Найдите баг: каждый раз, когда пользователь открывает и закрывает экран сканирования, расход памяти и CPU растёт и больше не падает.

    class _LiveScanState extends State<LiveScan> {  final _results = ReceivePort();  final _search = TextEditingController();  int _found = 0;  @override  void initState() {    super.initState();    _results.listen((msg) => setState(() => _found = msg as int));    Isolate.spawn(scanFolder, _results.sendPort, debugName: 'scanner');  }  @override  void dispose() {    _search.dispose();    super.dispose();  }}

    dispose не закрывает _results и не убивает воркер, поэтому после каждого визита остаются работающий изолят-сканер и открытый порт, чей слушатель держит мёртвый State живым и вызывает у него setState.

  11. 11

    Что SendPort.send делает с вашим объектом и почему 40 МБ в Uint8List отправляются намного дольше, чем 40 МБ в String?

    send синхронно и глубоко копирует весь граф объектов на отправляющем изоляте.

  12. 12

    Что такое группа изолятов и как Isolate.exit возвращает большой результат без копирования?

    Начиная с Dart 2.15, Isolate.spawn создаёт новый изолят в группе вызывающего: изоляты разделяют загруженную программу и одну управляемую кучу, хотя каждый видит только свои объекты.

    // one-shot worker: the result is handed over, not copiedFuture<Report> buildReport(List<OrderRow> rows) async {  final port = ReceivePort();  await Isolate.spawn(_build, (port.sendPort, rows));  return await port.first as Report;}…
  13. 13

    Что на самом деле экономит TransferableTypedData и когда обычный send с Uint8List ничем не хуже?

    Он делает каждую отправку передачей буфера вне кучи за постоянное время, но TransferableTypedData.fromList один раз копирует байты при создании.

    // worker: join the encoded chunks once, then move the buffervoid encoderMain(SendPort toMain) {  final chunks = <Uint8List>[for (final frame in frames()) encode(frame)];  final moved = TransferableTypedData.fromList(chunks); // the one copy  toMain.send(moved); // constant time, whatever the size  // moved.materialize(); // would throw ArgumentError: it left with the send…
  14. 14

    Вы вызываете Isolate.run на каждую строку списка, а сетка всё равно дёргается: когда долгоживущий воркер лучше, чем запуск изолята на каждую задачу?

    Когда работа одного вызова мала по сравнению с тем, что стоит и выбрасывает запуск изолята.

    class Thumbnailer {  // handshake as in the spawn example: the worker sends its SendPort first  Thumbnailer(this._requests);  final SendPort _requests;  // reply listener: _pending.remove(id)!.complete(bytes), or completeError(error)  final _pending = <int, Completer<Uint8List>>{};…
  15. 15

    Экспорт идёт тридцать секунд, и ему нужен прогресс-бар и рабочая кнопка «Отмена»: как получить и то, и другое от запущенного изолята?

    Прогресс передают через SendPort, в который воркер пишет по ходу работы.

    void exportMain(SendPort toMain) {  final control = ReceivePort(); // a ReceivePort cannot be sent,  toMain.send(control.sendPort); // so send its SendPort back first  var cancelled = false;  control.listen((msg) {    if (msg == 'cancel') cancelled = true;…
  16. 16

    Одно изображение на 40 мегапикселей занимает пул генерации миниатюр, и вся сетка встаёт: как выбрать размер пула и распределять задачи?

    Размер пула выбирайте по Platform.numberOfProcessors и бюджету памяти, а освободившийся воркер пусть сам берёт следующую задачу из одной общей очереди.

  17. 17

    compute() во Flutter web возвращает правильный ответ, но вешает вкладку на две секунды: что он там делает?

    На вебе нет изолятов Dart.

    // work.dart: one import for every platformexport 'work_io.dart' if (dart.library.js_interop) 'work_web.dart'; // work_io.dart (mobile and desktop): a real isolateFuture<int> countWords(String text) => Isolate.run(() => countWordsSync(text));…
  18. 18

    Память растёт, пока работает воркер экспорта, а CPU-профайлер показывает простаивающее приложение: как профилировать запущенный изолят?

    DevTools смотрит на один изолят за раз: CPU-профайлер, отладчик и снимки кучи следуют за изолятом, выбранным в селекторе изолятов, а по умолчанию это главный.