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

Ответ

Изоляты в 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-интенсивных операций.

Ответ 18+ 🔞

А, изоляты в Dart! Ну, это такая штука, когда тебе нужно, чтобы твоё приложение не зависало, как последний лох, пока там какие-то вычисления ебут мозг процессору. Представь, что у тебя есть один чувак (главный поток), который рисует интерфейс, а ему говорят: «Слушай, пересчитай-ка мне вот этот огромный массив данных, да побыстрее». Он начинает считать, весь в поту, и на интерфейс ему уже похуй — всё виснет, юзер орет. Вот чтобы такого не было, придумали изоляты.

По сути, это как отдельные квартиры в доме. У каждого изолята своя память, свой туалет (event loop), и они общаются не через общий холодильник (память), а кидаются записками в дверной проём (отправка сообщений). Гениально и безопасно, доверия ебать ноль между ними, и это хорошо.

Как этим пользоваться? Есть два основных подхода:

  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');
    }
  2. Низкоуровневый разговор через Isolate.spawn (для долгих отношений). Это когда тебе нужно не разово посчитать, а установить с изолятом постоянную связь, типа чата. Нужно создать порты для приёма сообщений (ReceivePort) и кидаться ими, как дети в песочнице лопатками.

    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),
      );
    
      // Ждём первое письмо от изолята — адрес его порта
      final isolateSendPort = await mainReceivePort.first as SendPort;
      isolateSendPort.send(21); // Отправляем ему число: «На, посчитай!»
    
      // Подписываемся на его ответы
      mainReceivePort.listen((message) {
        print('Пришло от изолята, ёпта: $message'); // Получим 42
      });
    }

А теперь, блядь, самое важное, что нужно запомнить, чтобы не охуеть потом:

  • Общей памяти НЕТ. Совсем. Это не потоки. Данные копируются или сериализуются. Если шлёшь свой кастомный класс, будь добр, научи его превращаться в JSON и обратно (toJson/fromJson), иначе получишь ошибку сериализации — и будешь сидеть, чесать репу.
  • Только top-level или static. Я уже говорил, но повторюсь. Функция для Isolate.spawn не может быть методом экземпляра или использовать замыкания из внешней области видимости. Изолят — не телепат, он живёт в другом мире.
  • Есть готовые решения. Если не хочешь возиться с портами вручную, посмотри пакеты вроде flutter_isolate или isolate. Они могут немного упростить жизнь, завернув это всё в более удобный API.
  • Не всё нужно в изолят. Если задача у тебя про ввод-вывод (скачать файл, спарсить JSON из сети), то обычные async/await справятся на ура, потому что они не грузят CPU, а ждут ответа от системы. Изоляты — это именно для тяжёлых вычислений, которые жрут процессорное время. Не создавай изолят, чтобы просто сделать HTTP-запрос, это ёперный театр.

Вот, в общем-то, и вся магия. Сначала кажется сложно, но потом, когда приложение перестаёт висеть на ровном месте, понимаешь — оно того стоит. Главное — не пытайся передать что-то, что не передаётся, и всё будет пизда рулю.