Реализация изолятов: spawn() и run()

В сегодняшней статье мы будем реализовывать изоляты. Но перед реализацией важно понять, что такое изоляты, чем они отличаются от async-await и когда их стоит использовать. Я уже подробно объяснил все эти моменты в моем предыдущем блоге, который вы можете прочитать здесь.

Также я создал репозиторий на GitHub с кодом этого проекта, с которым вы можете ознакомиться здесь.

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

Прежде всего, важно помнить, что функция для изолята должна быть объявлена вне всех классов. Это одна из самых распространенных ошибок, которую легко забыть и сложно отладить, так что обязательно обратите на это внимание.

Короткое напоминание о ReceivePort. ReceivePort — это двусторонний канал связи, где одна сторона может отправлять сообщения, а другая — их принимать и отвечать на них. Это обеспечивает двунаправленную связь между разными частями программы или между разными программами. Мы будем использовать его в нашем коде.

В этой статье мы будем вычислять сумму первых 1 000 000 чисел — это довольно ресурсоемкая задача. Если вы попробуете сделать это напрямую или с использованием async-await, то заметите, что интерфейс приложения начинает «лагать». Здесь же мы рассмотрим две разные реализации изолятов. Вы можете выбрать ту, которая лучше подходит под ваши требования.

Метод Isolate.run() [Короткоживущие изоляты]

Самый простой способ перенести процесс в изолят в Flutter — использовать метод Isolate.run. Этот метод создает изолят, передает ему функцию обратного вызова для начала вычислений, возвращает значение вычислений и затем завершает изолят после окончания вычислений. Все это происходит параллельно с основным изолятом и не блокирует его.

Метод Isolate.run требует одного аргумента — функции обратного вызова, которая будет запущена в новом изоляте. Подпись функции обратного вызова должна содержать ровно один обязательный неименованный аргумент. Когда вычисление завершается, функция возвращает значение обратно в основной изолят и завершает работу созданного изолята.

Пример: мы вычисляем сумму первых 10 000 000 чисел и возвращаем её. Не забудьте, что функция должна быть объявлена вне всех классов.

Future<double> complexTask3() async {
  const double iteration = 1000000000;
  final double result = await Isolate.run<double>(() {
    double total = 0.0;
    for (var i = 0; i < iteration; i++) {
      total += i;
    }
    return total;
  });
  return result;
}

Метод spawn() [Долгоживущие изоляты]

Короткоживущие изоляты удобны в использовании, но они имеют накладные расходы на создание новых изолятов и копирование объектов из одного изолята в другой. Если вы выполняете одно и то же вычисление с использованием Isolate.run несколько раз, возможно, вам будет выгоднее использовать изоляты, которые не завершаются сразу.

Когда вы используете метод Isolate.run, новый изолят завершает работу сразу после возвращения единственного сообщения в основной изолят. Иногда вам нужны изоляты, которые будут работать долго и смогут передавать друг другу несколько сообщений с течением времени. В Dart вы можете достичь этого с помощью API изолятов и портов. Такие долгоживущие изоляты называют фоновые рабочие процессы (background workers).

Долгоживущие изоляты полезны, когда у вас есть специфический процесс, который должен выполняться многократно в течение всего жизненного цикла вашего приложения, или если у вас есть процесс, который выполняется в течение длительного времени и должен возвращать несколько значений в основной изолят.

Ниже приведен код задачи, которую мы будем выполнять. И не забудьте поместить эту функцию вне всех классов, чтобы изолят смог ее запустить.

complexTask(SendPort sendPort) {
  var total = 0.0;
  for (var i = 0; i < 1000000000; i++) {
    total += i;
  }
  sendPort.send(total);
}

Здесь мы будем использовать функцию complexTask при нажатии на кнопку.

ElevatedButton(
  onPressed: () async {
    final receivePort = ReceivePort();
    await Isolate.spawn(complexTask, receivePort.sendPort);
    receivePort.listen((total) {
      debugPrint('Result 2: $total');
    });
  },
  child: const Text('Task 1'),
)

Теперь, каждый раз, когда вы нажимаете на эту кнопку, будет выполняться complexTask() без блокировки каких-либо операций интерфейса, и когда функция завершит свою задачу в другом изоляте, она вернет результат в основной изолят с использованием receivePort, так как вы слушаете его с помощью listen().

Предположим, вы хотите передать некоторые данные в изолят, что тогда? Это тоже можно сделать.

В этом примере предположим, что мы хотим передать конечное значение для цикла for. Тогда код будет выглядеть следующим образом:

ElevatedButton(
  onPressed: () async {
    final receivePort = ReceivePort();
    await Isolate.spawn(complexTask3, (iteration: 1000000000, sendPort: receivePort.sendPort));
    receivePort.listen((total) {
      debugPrint('Result 2: $total');
    });
  },
  child: const Text('Task 2'),
),
complexTask3(({int iteration, SendPort sendPort}) data) {
  var total = 0.0;
  for (var i = 0; i < data.iteration; i++) {
    total += i;
  }
  data.sendPort.send(total);
}

Таким образом, вы можете передавать столько переменных данных, сколько вам нужно.

У изолятов есть некоторые ограничения, о которых можно прочитать здесь.

Надеюсь, эта статья помогла вам лучше понять тему. Подписывайтесь на меня, чтобы не пропустить новые статьи.

Источник: https://medium.com/codex/isolate-implementation-explained-spawn-and-run-2cd8462c91ba


Flutter: async или isolate. Параллелизм

Многие не понимают разницу между async и isolate (параллелизмом и конкурентностью). В этой статье мы разберём эти понятия, а в следующей покажу, как реализовать изоляты в Dart.

ASYNC
Сначала разберём async, так как большинство из нас уже использовало этот подход хотя бы раз.

void readData() async {
  var url = "www.example.com";
  var content = await http.get(url); // Асинхронный вызов
  var data = parsingData(content); // Здесь будем использовать изолят, так как операция тяжелая
}

Этот код выполняется в обычном режиме до вызова загрузки файла, после чего программа ждёт, пока файл будет загружен. Пока она ждёт, другие части программы могут продолжать работать. Однако, обратите внимание, что в любой момент времени работает только одна часть приложения — ведь у нас всего один поток, один процессор. Асинхронность не делает ваше приложение многопоточным автоматически. После загрузки файла метод продолжит выполнение. Это и есть async. Когда задача (future) завершается, выполнение продолжается с следующей строки кода. В большинстве случаев async достаточно.

В примере выше наш код будет ждать на второй строке (await http.get(url)) до тех пор, пока данные не будут получены с интернета, и после этого мы начнем их обработку.

Теперь представьте, что файл очень большой, и его обработка требует много ресурсов процессора. Это может вызвать задержки в работе приложения? Многие могут подумать, что нет, так как вычисления происходят в асинхронном методе. Но это не так. Почему? Даже если метод асинхронный, в нём всё равно может происходить много синхронных операций. Эта обработка данных (parsingData) выполняется синхронно. Она занимает процессор, и другие части вашего приложения не смогут выполняться. Интерфейс пользователя не обновится, пока тяжелая работа не будет завершена, и пользователь увидит задержки и «подтормаживания».

Решение? Многопоточность с помощью изолятов!

ИЗОЛЯТЫ
Dart позволяет выполнять код параллельно с помощью изолятов. Изолят запускает другой процесс Dart, фактически, на другом потоке, на другом процессоре. Эти два изолята полностью изолированы друг от друга. Один не блокирует другой. Таким образом, ваше приложение может выполнять тяжелые вычисления и одновременно оставаться отзывчивым для пользователя. В этой статье я не буду углубляться в реализацию изолятов, так как это немного сложнее, и цель статьи — разъяснить разницу между async и изолятами.

Изолят можно представить как отдельное пространство на машине со своим собственным участком памяти и одним потоком, выполняющим события. В отличие от языков, таких как C++, где несколько потоков могут разделять одну память и выполнять произвольный код, в Dart каждый поток работает в своем собственном изоляте, с собственной памятью и обрабатывает только события.

Когда использовать изоляты?

Большинство приложений Dart работают в одном изоляте, но можно использовать больше, если это необходимо. Например, если вам нужно выполнить вычисление, которое может занять слишком много времени в основном изоляте и привести к «подтормаживанию», вы можете использовать Isolate.spawn() или функцию compute в Flutter, чтобы создать отдельный изолят для выполнения тяжелых вычислений, оставляя основной изолят свободным для перерисовки интерфейса.

Этот новый изолят получит свой собственный цикл событий и свою память, к которой родительский изолят не имеет доступа. Изоляты называются так потому, что они изолированы друг от друга. Они могут взаимодействовать только посредством передачи сообщений. Один изолят отправляет сообщение другому, и тот обрабатывает его в своем цикле событий.

Подведем итоги:
Async позволяет ждать выполнения других операций без блокировки текущего потока. Async не создаёт отдельный поток; Dart просто переключается между разными частями программы по мере необходимости. Но если необходимо выполнение параллельных задач, вам нужны изоляты. Они сложнее в настройке, но позволяют решать задачи, требующие интенсивных вычислений, без задержек в работе пользовательского интерфейса.

Изоляты играют важную роль в обеспечении конкурентной обработки. Выполнение ресурсоемких операций в основном потоке может негативно сказаться на производительности приложения. Использование изолятов позволяет значительно повысить производительность и эффективность приложений Flutter.

Источник: https://medium.com/@AryanBeast/async-vs-isolate-in-flutter-parallelism-in-flutter-ae3954fb5d4c