Как работать с изолятами (Isolates) в Dart для многопоточности?

«Как работать с изолятами (Isolates) в Dart для многопоточности?» — вопрос из категории Dart Core, который задают на 36% собеседований Flutter Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Изоляты в Dart — это модель параллелизма, основанная на передаче сообщений. Каждый изолят имеет свою собственную память и event loop, что позволяет выполнять тяжелые вычисления, не блокируя основной поток UI.

Основные подходы:

  1. Функция compute (для разовых задач): Идеально подходит для вызова чистой функции с тяжелыми вычислениями.

    import 'package:flutter/foundation.dart';
    
    // Функция ДОЛЖНА быть top-level или static.
    int _heavyComputation(int input) {
      // Имитация долгого вычисления (например, обработка изображения).
      int result = 0;
      for (int i = 0; i < input * 1000000; i++) {
        result += i.isEven ? 1 : -1;
      }
      return result;
    }
    
    // Вызов в коде UI
    void onButtonPressed() async {
      final result = await compute(_heavyComputation, 100);
      print('Result from isolate: $result');
    }
  2. Низкоуровневый API с Isolate.spawn (для долгоживущих задач): Используется, когда нужно поддерживать постоянный канал связи.

    import 'dart:isolate';
    
    // Сообщение для изолята
    class IsolateMessage {
      final SendPort sendPort; // Для ответа
      final int data;
      IsolateMessage(this.sendPort, this.data);
    }
    
    // Функция, запускаемая в изоляте
    static void _isolateEntryPoint(IsolateMessage initialMessage) async {
      final port = ReceivePort();
      initialMessage.sendPort.send(port.sendPort); // Отправляем обратный порт
    
      await for (final message in port) {
        if (message is int) {
          final result = message * 2;
          initialMessage.sendPort.send(result); // Отправляем результат
        }
      }
    }
    
    // Запуск изолята в основном коде
    Future<void> startLongRunningIsolate() async {
      final mainReceivePort = ReceivePort();
    
      await Isolate.spawn(
        _isolateEntryPoint,
        IsolateMessage(mainReceivePort.sendPort, 0),
      );
    
      // Получаем sendPort от изолята для дальнейшей связи
      final isolateSendPort = await mainReceivePort.first as SendPort;
      isolateSendPort.send(21); // Отправляем задание
    
      mainReceivePort.listen((message) {
        print('Received from isolate: $message'); // 42
      });
    }

Критически важные ограничения и практика:

  • Нет общей памяти: Данные передаются через копирование (для примитивов, List, Map, Set) или сериализацию. Объекты ваших классов должны реализовывать fromJson/toJson.
  • Только top-level или static функции: Функция, передаваемая в Isolate.spawn, не может быть методом экземпляра или использовать замыкания.
  • Пакет isolate для удобства: Для более удобной работы с передачей сложных сообщений и управления жизненным циклом можно использовать пакеты, такие как flutter_isolate (для Flutter) или isolate.
  • Альтернативы: Для задач, связанных с вводом-выводом (сеть, файлы), часто достаточно использовать async/await, так как они не блокируют поток. Изоляты нужны именно для CPU-интенсивных операций.