Реализация изолятов: 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


Leave a Comment