Какие инструменты для работы с многопоточностью в Dart/Flutter ты знаешь?

Ответ

В Dart/Flutter я работаю с многопоточностью через изоляты (isolates), так как Dart использует модель акторов с разделяемой-nothing архитектурой:

1. Базовые изоляты для CPU-intensive задач:

// Создание простого изолята
Future<void> runInIsolate() async {
  final receivePort = ReceivePort();

  await Isolate.spawn(
    _heavyComputation,
    receivePort.sendPort, // Передаем порт для коммуникации
  );

  // Получаем результат из изолята
  final result = await receivePort.first;
  print('Result from isolate: $result');
}

void _heavyComputation(SendPort sendPort) {
  // Этот код выполняется в отдельном изоляте
  final result = performExpensiveCalculation();
  sendPort.send(result);
}

2. Упрощенный API через compute():

// Для одноразовых задач
int fibonacci(int n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

void calculateFibonacci() async {
  // compute автоматически создает изолят, выполняет функцию
  // и возвращает результат
  final result = await compute(fibonacci, 40);
  print('Fibonacci(40) = $result');
}

3. Продвинутые сценарии с несколькими изолятами:

// Создание пула изолятов для параллельной обработки
class IsolatePool {
  final List<Isolate> _isolates = [];

  Future<void> initialize({int count = 4}) async {
    for (int i = 0; i < count; i++) {
      final isolate = await Isolate.spawn(_worker, i);
      _isolates.add(isolate);
    }
  }

  static void _worker(int id) {
    print('Isolate $id started');
    // Логика worker-изолята
  }
}

4. Коммуникация между изолятами:

// Двусторонняя коммуникация через порты
Future<SendPort> createTwoWayCommunication() async {
  final receivePort = ReceivePort();

  await Isolate.spawn(
    _isolateEntry,
    receivePort.sendPort,
  );

  // Получаем порт для отправки сообщений в изолят
  final sendPortToIsolate = await receivePort.first as SendPort;

  // Создаем порт для получения ответов
  final responsePort = ReceivePort();
  sendPortToIsolate.send({
    'message': 'Hello from main isolate',
    'replyTo': responsePort.sendPort,
  });

  return sendPortToIsolate;
}

5. Важные ограничения и best practices:

  • Изоляты не разделяют память, все данные должны быть сериализуемы
  • compute() не подходит для задач, требующих постоянной коммуникации
  • Для UI-обновлений из изолята используйте IsolateNameServer или StreamController с StreamBuilder
  • Избегайте создания большого количества изолятов (обычно 2-4 на CPU core)

Ответ 18+ 🔞

Слушай, ну вот реально, какого хуя все думают, что многопоточность в Dart — это какая-то магия? Ёпта, тут всё просто, как три копейки, но со своими приколами. Сидишь ты такой, пишешь свой красивый UI на Flutter, а потом бац — нужно тяжёлую математику посчитать. И твой интерфейс встаёт колом, как будто его мартышлюшка с сервера прибила. Волнение ебать! А всё потому, что Dart — однопоточная зверюга по умолчанию. Но выход есть, и зовут его изоляты.

Представь себе: у тебя не потоки, а отдельные вселенные. Каждая живёт сама по себе, своей памятью, своим процессом. Никакого общего доступа к переменным, никаких мьютексов — красота же! Это и есть модель акторов, где эти вселенные-изоляты общаются только почтой — отправляют друг другу сообщения. Доверия ебать ноль, зато и гонок данных нет.

1. Ну, для начала, самый простой способ — породить изолят вручную.

Смотри, как это выглядит. Ты создаёшь почтовый ящик (ReceivePort), чтобы получать письма. Потом говоришь системе: «Слушай, роди мне отдельную вселенную, вот тебе адрес, куда слать ответ». И в этой вселенной выполняется твоя тяжёлая функция.

Future<void> runInIsolate() async {
  final receivePort = ReceivePort(); // Почтовый ящик создал

  await Isolate.spawn(
    _heavyComputation, // Функция, которая будет в изоляте крутиться
    receivePort.sendPort, // Адрес своей почты передал
  );

  // Сидишь, ждёшь первое письмо из ящика
  final result = await receivePort.first;
  print('Result from isolate: $result'); // Опа, результат пришёл!
}

void _heavyComputation(SendPort sendPort) {
  // А это уже код в параллельной вселенной, в изоляте
  final result = performExpensiveCalculation(); // Считаешь что-то долгое
  sendPort.send(result); // Кидаешь результат обратно в главный мир
}

Прям как в космос сигнал послал и ждёшь ответа. Главное помнить: всё, что ты туда передаёшь и обратно получаешь, должно уметь в сериализацию. Никаких кастомных классов с методами туда-сюда не перешлёшь — только примитивы, списки, мапы. Иначе получишь хитрую жопу в виде ошибки.

2. Но если тебе лень возиться с портами вручную, есть готовая обёртка compute().

Это, бля, просто подарок для ленивых. Хочешь посчитать что-то одноразовое — оборачиваешь свою функцию в compute, и система сама создаст изолят, выполнит там твой код и вернёт тебе Future с результатом. Красота!

// Обычная функция, которая много думает
int fibonacci(int n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2); // Рекурсия, ёбать колотить
}

void calculateFibonacci() async {
  // compute — волшебный пендель. Передал функцию и аргумент.
  final result = await compute(fibonacci, 40);
  print('Fibonacci(40) = $result'); // И не надо париться с портами
}

Но, чувак, не обольщайся. compute — это для разовых акций. Если тебе нужно постоянное общение с изолятом (типа воркера, который живёт и ждёт заданий), то это не твой вариант. Тут уже надо ручками работать.

3. Для серьёзных дел — пулы изолятов.

Допустим, у тебя овердохуища задач, и создавать изолят на каждую — это как из пушки по воробьям. Тогда можно сделать пул: создать несколько изолятов заранее и кормить их работой по очереди.

class IsolatePool {
  final List<Isolate> _isolates = []; // Список наших работяг

  Future<void> initialize({int count = 4}) async {
    for (int i = 0; i < count; i++) {
      // Породил изолят, дал ему ID
      final isolate = await Isolate.spawn(_worker, i);
      _isolates.add(isolate);
    }
  }

  static void _worker(int id) {
    print('Isolate $id started'); // Изолят говорит: «Я живой!»
    // Тут он может висеть в бесконечном цикле и ждать задач
  }
}

Это уже ближе к промышленному использованию. Но и сложности прибавляется: нужно продумать, как распределять задачи, как собирать результаты, как закрывать изоляты, когда они не нужны. А то будут висеть, как сосалки, ресурсы жрать.

4. А если нужна полноценная двусторонняя связь?

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

Future<SendPort> createTwoWayCommunication() async {
  final receivePort = ReceivePort(); // Ящик для получения порта от изолята

  await Isolate.spawn(
    _isolateEntry, // Точка входа в изолят
    receivePort.sendPort, // Отправляем свой адрес на тот свет
  );

  // Ждём, когда изолят пришлёт нам СВОЙ адрес для писем
  final sendPortToIsolate = await receivePort.first as SendPort;

  // Теперь можем ему что-то отправить. Создаём ящик для ответа.
  final responsePort = ReceivePort();
  sendPortToIsolate.send({
    'message': 'Hello from main isolate',
    'replyTo': responsePort.sendPort, // И свой новый адрес для ответа прикладываем
  });

  // Можем ждать ответа на responsePort.first
  return sendPortToIsolate; // Или вернуть порт для будущего общения
}

Выглядит немного запутанно, но логика простая: обменялись адресами — теперь можем болтать. Главное, не потерять SendPort — это твой единственный способ что-то сказать этому изоляту.

5. И напоследок, важные подводные камни.

Запоминай, терпения ноль ебать с этими штуками, если не соблюдать правила:

  • Сериализуемость — святое. В изолят летят только копии данных. Большие объекты — это время и память. Иногда проще в изоляте данные с нуля создать.
  • compute() — для простоты. Не пытайся запихнуть туда что-то сложное с кучей колбэков. Не выйдет.
  • UI-поток священен. Нельзя из изолята напрямую лезть в виджеты. Всё через setState, StreamBuilder или IsolateNameServer (это такая телефонная книга для изолятов, чтобы найти друг друга по имени).
  • Не плоди сущности. Создание изолята — дорого. Не делай их, как гомосеки налетели на каждый чих. Оптимально — по количеству ядер процессора (2-4 штуки). Больше — будет только просадка по производительности.

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