4.6. Advanced изоляты

Основа пользовательского опыта — это скорость работы приложения. Наверняка вы сталкивались с приложениями, у которых «подвисает» интерфейс. Согласитесь, это не самое приятное ощущение. Чтобы не допустить подобных проблем в своих проектах, нужно уделять время оптимизации.

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

Параллелизм

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

А про параллелизм поговорим сейчас. Как следует из названия, это выполнение действий параллельно, независимо друг от друга. Звучит похоже на асинхронность, но есть важное отличие. Покажем на примере.

В понедельник утром рыбак Вася отправился на своей лодке в море. Доплыв до рыбного места, он взял две удочки, нацепил на кончики по колокольчику и закинул их по очереди в воду. Как только звучал звонок, рыбак подбегал к нужной удочке, доставал улов и снова закидывал леску в воду. Иногда сигнал подавали сразу обе удочки, но Вася разорваться не мог и всё равно доставал улов поочерёдно. Иногда он успевал поймать рыбу, иногда нет.

Так и прошёл его день — он ходил то к одной удочке, то к другой, а в периоды затишья читал книгу. В данном случае рыбак работал по асинхронной модели.

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

Теперь переложим это на Flutter. Для начала вспомним, что Flutter написан на Dart, а Dart — это однопоточный язык программирования. Это значит, что во Flutter-приложении одновременно может выполняться только одна операция. И, пока она выполняется, приложение остаётся «заблокированным».

  • Асинхронность, как в примере с рыбаком Васей, означает, что задачи могут выполняться по очереди, не блокируя основной поток. Во Flutter это достигается с помощью Future или async/await, что позволяет выполнять операции без остановки интерфейса.

  • Параллелизм реализуется через изоляты. Изолят во Flutter — это аналог потока, который выполняет задачу параллельно с другими изолятами. Каждый изолят имеет собственную память, и их работа не влияет друг на друга. Это позволяет использовать несколько потоков для выполнения задач одновременно, эффективно распределяя нагрузку и ускоряя выполнение кода.

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

Что такое поток

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

Это хороший инструмент для разработки эффективных и масштабируемых приложений. Однако следует знать и о проблемах, с которыми можно столкнуться при работе с потоками.

  1. Состояние гонки (race condition) — это ситуация, когда результат работы программы зависит от порядка выполнения частей кода, которые могут выполняться параллельно.

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

    Например, рассмотрим ситуацию, когда два потока одновременно пытаются увеличить значение переменной count на &единицу. Для этого требуется выполнить несколько операций: чтение, увеличение и запись значения переменной. Если один поток выполнит операцию чтения значения переменной до того, как другой поток выполнит операцию записи нового значения, то значение переменной может быть увеличено только на единицу вместо двух.

  2. Взаимоблокировка данных (deadlock) — это ситуация, когда два или более потока или процесса блокируют друг друга, ожидая освобождения ресурсов.

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

    Например, рассмотрим ситуацию, когда один поток удерживает ресурс A, а другой поток удерживает ресурс B. Если каждый из потоков попытается получить доступ к ресурсу, который удерживает другой поток, то возникнет взаимоблокировка.

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

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

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

Коммуникация между изолятами происходит с помощью сообщений. Каждый изолят содержит свой собственный EventLoop c очередью микрозадач (microtask) и событий (event).

Мы помним, что каждый раз при запуске Flutter-приложения создается изолят, в котором запускается EventLoop. Изоляты сгруппированы в изолированные группы (IsolateGroup). Изоляты внутри группы используют одну и ту же кучу (heap), управляемую сборщиком мусора, которая используется в качестве хранилища для объектов, выделенных изолятом, а также делят код.

Совместное использование кучи между изолятами в одной группе — это деталь реализации, которая не просматривается из кода Dart. Даже изоляты внутри одной группы не могут напрямую обмениваться изменяемыми состояниями и могут взаимодействовать только с помощью передачи сообщений через порты. Если требуется создать новую группу, можно воспользоваться специальным методом Isolate.spawnUri (этот метод недоступен во Flutter). Isolate.spawn же создаёт новый изолят в отдельной группе, если этот метод запускается из main, и в той же группе, если из существующего изолята, уже присоединенного к группе.

Подробнее о куче

Куча (heap) — область оперативной памяти, в которой хранятся объекты Dart, созданные с помощью конструктора класса (например, с помощью MyClass()). Память в куче управляется виртуальной машиной Dart VM. Виртуальная машина выделяет память для объекта в момент его создания и освобождает память, когда объект больше не используется.

Add text to be displayed on click

В пределах одной группы изолятов при передаче сообщения через порт используется следующая логика:

  • для примитивных типов выполняется сериализация в любом случае (при использовании TransferableTypedData передаётся указатель, из которого выполняется копирование в память нового изолята);

  • для объектов кучи в пределах одной группы изолятов выполняется копирование внутреннего представления и выделение памяти в памяти нового изолята (без полной сериализации);

  • в случае несовпадения групп изолятов выполняется полная сериализация объекта и повторная материализация его в контексте изолята получателя порта.

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

Нет конкретных правил для использования изолятов, но всё же приведем ситуации, в которых изоляты помогут повысить производительность:

  • сериализация и десериализация больших JSON;

  • обработка и сжатие фотографий, аудио и видео;

  • конвертирование аудио- и видеофайлов;

  • выполнение поиска и фильтрации в больших списках или внутри файловых систем;

  • выполнение операций ввода-вывода, таких как обмен данными с базой данных;

  • обработка большого объёма сетевых запросов.

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

Как создать изолят

Есть несколько способов, и выбор зависит от ваших потребностей. Разберём эти варианты ниже.

Compute

Самый простой способ выполнить код в другом изоляте — метод compute из пакета foundation.

1Future<R> compute<M, R>(
2	ComputeCallback<M, R> callback,
3	M message, {
4	String? debugLabel,
5})
6

В данном случае callback — функция верхнего уровня, которая не должна являться методом какого-либо класса (нельзя ссылаться на методы, требующие создания экземпляра класса). А message — переменная, аргумент для callback, который необходимо передать, но не обязательно использовать.

Реализация метода compute во Flutter Framework может выглядеть следующим образом:

1Future<R> compute<M, R>(isolates.ComputeCallback<M, R> callback, M message, {String? debugLabel}) async {
2  debugLabel ??= kReleaseMode ? 'compute' : callback.toString();
3
4  return Isolate.run<R>(() {
5    return callback(message);
6  }, debugName: debugLabel);
7}
8

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

Пример использования функции compute:

1Future<bool> isPrime(int value) {
2  return compute(_calculate, value);
3}
4
5bool _calculate(int value) {
6  if (value == 1) {
7    return false;
8  }
9  for (int i = 2; i < value; ++i) {
10    if (value % i == 0) {
11      return false;
12    }
13  }
14  return true;
15}
16

Isolate.run

Тот самый метод, что находится «под капотом» у compute. Это также упрощённый интерфейс для работы с изолятами. Методы compute и Isolate.run взаимозаменяемы, за исключением того, что compute недоступен для использования в языке Dart, только вместе с фреймворком Flutter.

1Future<R> run<R>(
2	FutureOr<R> computation(), {
3	String? debugName,
4})
5

Используйте compute/Isolate.run для единичных вычислений. Например, для вычисления числа Фибоначчи:

1int slowFib(int n) =>
2    n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
3
4// Вычисление без блокировки главного изолята
5final fib40 = await Isolate.run(() => slowFib(40));
6

Низкоуровневое решение

Это решение не использует никаких пакетов и полностью полагается на низкоуровневый API, предлагаемый Dart. Рассмотрим его применение пошагово.

Шаг 1. Создание и рукопожатие

Для создания изолята воспользуемся методом spawn. Он создаёт и порождает изолят, который использует тот же код, что и текущий изолят (то есть создаёт изолят в той же группе).

1Future<Isolate> spawn<T>(
2	void entryPoint( T message ),
3	T message, {
4	bool paused = false,
5	bool errorsAreFatal = true,
6	SendPort? onExit,
7	SendPort? onError,
8	@Since("2.3") String? debugName,
9})
10

Аргумент entryPoint указывает начальную функцию для вызова в созданном изоляте. Функция точки входа вызывается в новом изоляте с помощью message в качестве единственного аргумента.

Каждый изолят в Dart имеет специальный порт, называемый controlPort , который предназначен для передачи управляющих сообщений. Обычно мы явно не используем это свойство, так как можем выполнить такие действия, как завершение изолята и его остановка, используя другие средства управления. Тем не менее нужно знать, что controlPort идентифицирует изолят и защищает доступ к некоторым операциям управления, используя «возможности» (terminateCapability и pauseCapability).

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

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

ReceivePort — это не широковещательный поток. Это означает, что он буферизует входящие сообщения до тех пор, пока не будет зарегистрирован слушатель. Только один слушатель может получать сообщения. Воспользуйтесь Stream.asBroadcastStream для преобразования порта в широковещательный поток:

1// Создаем ReceivePort
2ReceivePort receivePort = ReceivePort();
3
4// Преобразуем ReceivePort в широковещательный поток.
5Stream broadcastStream = receivePort.asBroadcastStream();
6
7

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

У ReceivePort может быть много портов отправки.

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

1//
2// Порт нового изолята.
3// Будет использоваться для дальнейшей
4// отправки сообщений в новый изолят.
5//
6SendPort newIsolateSendPort;
7
8//
9// Экземпляр нового изолята.
10//
11Isolate newIsolate;
12
13//
14// Метод, который запускает новый изолят
15// и выполняет инициализацию с рукопожатием.
16//
17void callerCreateIsolate() async {
18    //
19    // Локальный ReceivePort для
20    // последующей передачи SendPort
21    //
22    ReceivePort receivePort = ReceivePort();
23
24    //
25    // Инициализация нового изолята.
26    //
27    newIsolate = await Isolate.spawn(
28        callbackFunction,
29        receivePort.sendPort,
30    );
31
32    //
33    // Извлечение порта для произведения
34    // последующего общения.
35    //
36    newIsolateSendPort = await receivePort.first;
37}
38
39//
40// Точка входа нового изолята.
41//
42static void callbackFunction(SendPort callerSendPort){
43    //
44    // Экземпляр SendPort для передачи сообщений
45    // от "вызывающего абонента".
46    //
47    ReceivePort newIsolateReceivePort = ReceivePort();
48
49    //
50    // Передаём ссылку на SendPort данного изолята "вызывающему абоненту".
51    //
52    callerSendPort.send(newIsolateReceivePort.sendPort);
53
54    //
55    // Дальнейшие вычисления.
56    //
57}
58

Важно помнить, что «точка входа» изолята должна быть функцией верхнего уровня или статическим методом. Точкой входа может быть и замыкание, но в этом случае данные, которые захватывает замыкание, также будут неявно переданы в изолят (это может вызвать проблемы с производительностью, увеличение использования памяти).

Шаг 2. Отправка сообщения в изолят

Для начала определим, какие объекты вы можете передать в изолят. В общем случае это:

  • null

  • Булевые переменные

  • Экземпляры int, double, String, num

  • Экземпляры, созданные с помощью литералов коллекций List, Map, Set

  • Экземпляры, созданные через конструкторы:

  • SendPort-экземпляры из ReceivePort.SendPort или RawReceivePort.SendPort, где порты приёма создаются с помощью конструкторов этих классов.

  • Экземпляры, представляющие один из типов, упомянутых выше, Object, dynamic, void и Never , варианты с null-значением, а также объекты, аргументы которых являются отправляемыми типами.

Если изоляты отправителя и получателя используют один и тот же код (например, изоляты, созданные с помощью Isolate.spawn), передаваемые объекты в message могут содержать любой объект, за следующими исключениями:

  • Объекты с собственными ресурсами (подклассы, например, NativeFieldWrapperClass1). Socket-объект, например, внутренне ссылается на объекты, к которым подключены собственные ресурсы, и поэтому не может быть отправлен.

  • ReceivePort

  • DynamicLibrary

  • Finalizable

  • Finalizer

  • NativeFinalizer

  • Pointer

  • UserTag

  • MirrorReference

Теперь, когда у нас есть порт, который будет использоваться для отправки сообщения в Isolate, давайте посмотрим, как это сделать:

1//
2// Метод для отправления сообщения в новый изолят.
3// Возвращает ответ.
4//
5Future<String> sendReceive(String messageToBeSent) async {
6    //
7    // Создаём порт для извлечения ответа.
8    //
9    ReceivePort port = ReceivePort();
10
11    //
12    // Передаём изоляту сообщения, а также
13    // порт, в который нужно передать ответ.
14    //
15    newIsolateSendPort.send(
16        CrossIsolatesMessage<String>(
17            sender: port.sendPort,
18            message: messageToBeSent,
19        )
20    );
21
22    //
23    // Ожидаем ответ и возвращаем его.
24    //
25    return port.first;
26}
27
28//
29// Обрабатывает входящие сообщения.
30//
31static void callbackFunction(SendPort callerSendPort){
32    //
33    // Экземпляр SendPort для получения сообщений
34    // от "вызывающего абонента".
35    //
36    ReceivePort newIsolateReceivePort = ReceivePort();
37
38    //
39    // Передача "вызывающему абоненту" ссылки на порт данного изолята.
40    //
41    callerSendPort.send(newIsolateReceivePort.sendPort);
42
43    //
44    // Прослушиваем входящие сообщения,
45    // обрабатываем их и возвращаем результат.
46    //
47    newIsolateReceivePort.listen((dynamic message){
48        CrossIsolatesMessage incomingMessage = message as CrossIsolatesMessage;
49
50        //
51        // Обработка сообщения.
52        //
53        String newMessage = "complemented string " + incomingMessage.message;
54
55        //
56        // Выполняем результат обработки.
57        //
58        incomingMessage.sender.send(newMessage);
59    });
60}
61
62//
63// Вспомогательный класс.
64//
65class CrossIsolatesMessage<T> {
66    final SendPort sender;
67    final T message;
68
69    CrossIsolatesMessage({
70        required this.sender,
71        this.message,
72    });
73}
74

Шаг 3. Уничтожение нового изолята

Самым верным завершением работы изолята является окончание работы его EventLoop. Но также API изолятов в Dart предоставляет нам несколько методов для выполнения этой задачи:

  • метод kill;

  • метод exit.

Метод kill запрашивает у изолята выполнить завершение работы.

1void kill({
2	int priority = beforeNextEvent,
3})
4

В качестве аргумента ожидается поле priority, принимающее значения immediate или beforeNextEvent. Завершение работы произойдёт в разные моменты в зависимости от выбранного приоритета:

  • immediate— изолят завершает работу как можно скорее. Сообщения обрабатываются по порядку, поэтому все ранее отправленные события из этого изолята будут обработаны. Завершение работы должно произойти не позже, чем при отправке с beforeNextEvent. Это может произойти раньше, если в системе есть способ завершить работу раньше, даже во время выполнения другого события.

  • beforeNextEvent — завершение работы запланировано на следующий раз, когда EventLoop вернётся в цикл событий принимающего изолята, после завершения текущего события и любых уже запланированных событий управления.

Если свойство изолята terminateCapabilitynull (то есть «не определён в ControlPort»). В этом случае запрос на завершение работы игнорируется принимающим изолятом.

Метод exit завершает работу текущего изолята синхронно.

1Never exit([ 
2	SendPort? finalMessagePort, 
3	Object? message 
4])
5

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

При успешном выполнении метод не возвращает никакого значения, и ваши условия finally не будут выполнены.

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

1void dispose(){
2    newIsolate?.kill(priority: Isolate.immediate);
3    newIsolate = null;
4}
5

Работа с платформенным кодом

Начиная с версии Flutter 3.7 вы можете использовать плагины платформы в фоновых изолятах. Это открывает множество возможностей для переноса тяжёлых, зависящих от платформы вычислений в изолят, который не будет блокировать ваш пользовательский интерфейс.

Пример

Хороший пример фоновой работы с плагинами — фитнес-приложение, которое получает данные пульсометра с помощью Bluetooth-соединения и вычисляет количество пройденных шагов на основе данных акселерометра и гироскопа. Работа с Bluetooth, акселерометром и гироскопом во Flutter-приложении становится возможна за счёт плагинов.

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

Add text to be displayed on click

Платформенные каналы изолята используют API BackgroundIsolateBinaryMessenger. Следующий фрагмент показывает пример использования пакета shared_preferences в фоновом изоляте.

1import 'dart:isolate';
2
3import 'package:flutter/services.dart';
4import 'package:shared_preferences/shared_preferences.dart';
5
6void main() {
7  // Определите RootIsolateToken для передачи в изолят.
8  RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
9  Isolate.spawn(_isolateMain, rootIsolateToken);
10}
11
12Future<void> _isolateMain(RootIsolateToken rootIsolateToken) async {
13  // Зарегистрируйте изолят в качестве фонового по отношению к корневому.
14  BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
15
16  // Теперь вы можете пользоваться плагином shared_preferences.
17  SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
18
19  print(sharedPreferences.getBool('isDebug'));
20}
21

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

Плагины и платформенные каналы могут быть использованы любым изолятом, попадающим под следующие условия:

  • изолят является корневым (созданным Flutter);

  • изолят зарегистрирован в качестве фонового по отношению к корневому.

Регистрация фонового изолята происходит посредством RootIsolateToken (токен, представляющий корневой изолят).

Отладка изолятов

Вы можете получить информацию о текущем состоянии изолятов и портов в Dart через Dart VM Service Protocol и инструменты DevTools.

Информация об изолятах:

  • Используйте команду getVM в Dart VM Service Protocol для получения списка активных изолятов.

  • В DevTools вы можете просмотреть список изолятов на вкладке VM в разделе VM Tools.

Информация о портах:

  • Команда getPorts в Dart VM Service Protocol позволяет получить информацию о портах, связанных с receiver-частью.

  • В DevTools список портов находится на вкладке Isolates в разделе VM Tools.

Работу DevTools обеспечивает VM Service. Это библиотека, которая позволяет взаимодействовать с сервисным протоколом Dart VM. Этот протокол предоставляет низкоуровневый интерфейс для управления и мониторинга выполнения приложений Dart. VM Service используется в основном для отладки, профилирования и других задач диагностики приложений.

Вот несколько основных возможностей, которые предоставляет VM Service:

  1. Отладка. Вы можете управлять выполнением программы, включая установку точек останова и контроль за стеком вызовов.

  2. Профилирование. Сбор данных о производительности, например использование CPU и памяти, для оптимизации приложений.

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

  4. Управление памятью. Возможность получения информации о распределении объектов в куче и выполнения сборок мусора.

  5. Взаимодействие с изолятами. VM Service может предоставить информацию о каждом изоляте, такую как текущий статус, используемые ресурсы и информация о стеке вызовов.

Когда вы запускаете приложение Flutter в режиме отладки, Dart VM автоматически включает VM Service. Flutter создаёт экземпляр VM Service и начинает прослушивать соединения по определённому порту на localhost.

Этот порт может варьироваться, что позволяет запускать несколько приложений одновременно. При старте приложения в командной строке обычно выводится URL VM Service. DevTools использует этот URL для подключения к VM Service. Кроме того, вы можете подключиться к VM Service с помощью внешнего клиента, поддерживающего gRPC, что позволяет интегрировать и взаимодействовать с VM Service на низком уровне.

Если вы используете командную строку, вы можете запустить DevTools вручную, используя команду flutter pub global run devtools, и затем открыть предоставленный URL DevTools. После установления соединения DevTools может запрашивать данные и отправлять команды в VM Service. Это включает получение информации о производительности, состоянии виджетов, структуре изолятов и многом другом.

VM Service работает в контексте отдельного изолята с названием vm-service, который относится к категории System Isolates в DevTools. Таким образом его задачи по сбору данных и управлению не мешают основному потоку выполнения вашего приложения.

Управление состоянием изолята

Для управления состоянием изолята следует пользоваться следующими методами:

  • pause:
1Capability pause([ 
2	Capability? resumeCapability 
3])
4

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

Когда изолят получает команду pause, он прекращает обработку EventLoop. Он всё ещё может добавлять новые события в очередь, например, на таймеры или сообщения ReceivePort. Когда изолят возобновляется, EventLoop начинает обрабатывать уже поставленные в очередь события.

Запрос на паузу отправляется через командный порт изолята, который обходит EventLoop принимающего изолята. Пауза вступает в силу при её получении, приостанавливая EventLoop в том виде, в каком он есть на данный момент.

Параметр resumeCapability идентифицирует паузу и должен использоваться снова для завершения паузы с помощью resume. Если параметр resumeCapability опущен, вместо него создаётся и используется новый объект Capability.

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

Если изолят приостановлен с использованием более чем одной возможности, каждая пауза должна завершаться по отдельности, прежде чем изоляция возобновится.

  • resume:
1void resume( Capability resumeCapability )
2

Возобновляет работу приостановленного изолята. Отправляет сообщение изоляту с запросом о завершении паузы, которая была запрошена ранее.

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

Если resumeCapability ранее не использовался для приостановки изолята или он уже использовался для возобновления с этой паузы, вызов resume не имеет эффекта.

  • ping:
1void ping(
2	SendPort responsePort, {
3	Object? response,
4	int priority = immediate,
5})
6

Запрашивает изолят отправить response на responsePort.

Объект response должен соответствовать тем же ограничениям, которые применяются SendPort.send при отправке изоляту из другой изолированной группы: разрешены только простые значения, которые могут быть отправлены всем изолятам, такие как null логические значения, числа или строки.

Если изолят активен, он в конечном итоге отправит response (значение по умолчанию равно null) на порт ответа.

Поле priority должно быть одним из immediate или beforeNextEvent. Ответ отправляется в разное время в зависимости от типа ping:

  • immediate: изолят отвечает, как только получает управляющее сообщение. Это происходит после получения и обработки любого предыдущего управляющего сообщения из того же изолята, но может произойти во время выполнения другого события.

  • beforeNextEvent: ответ запланирован на следующий раз, когда элемент управления вернется в EventLoop принимающего изолята после завершения текущего события и всех уже запланированных событий управления.

Ошибки и их обработка

При работе с изолятами вы можете столкнуться со следующими ошибками:

  • IsolateSpawnException: исключение, не удалось создать изолят.

  • RemoteError: необработанная ошибка из изолята.

Далее рассмотрим, какие инструменты для их обработки нам предоставляет Dart.

ErrorListener

Чтобы слушать необработанные ошибки изолята, можно воспользоваться методом addErrorListener. Запросы, которые не были обнаружены в изоляте, отправляются обратно на port, их можно получить, воспользовавшись методом ReceivePort listen следующим образом:

1ReceivePort port = ReceivePort();
2port.listen((message) {
3  if (message is List && message.length == 2) {
4    var error = message[0];      // Описание ошибки или объект ошибки.
5    var stackTrace = message[1]; // Стек вызовов.
6    print('Error: $error');
7    print('Stack Trace: $stackTrace');
8  }
9});
10

Ошибки отправляются обратно в виде списков из двух элементов. Первый элемент — это String представление ошибки, обычно создаваемое вызовом toString при ошибке. Второй элемент — это String представление сопутствующей трассировки стека, или null, если трассировка стека не была предоставлена. Чтобы преобразовать это обратно в объект StackTrace, используйте StackTrace.fromString.

Прослушивание с использованием одного и того же порта более одного раза ничего не даёт. Порт получит каждую ошибку только один раз, и его нужно будет удалить только один раз с помощью removeErrorListener.

Закрытие порта приёма, который связан с портом, не останавливает изолят от отправки неперехваченных ошибок, они просто будут потеряны. Вместо этого используйте removeErrorListener чтобы прекратить приём ошибок на port.

setErrorsFatal

Данный метод устанавливает, будут ли не перехваченные ошибки завершать изолят. Если ошибки являются фатальными, любая не перехваченная ошибка завершит EventLoop изолята и, соответственно, завершит работу изолята.

Для этого вызова требуется terminateCapability для изолята. Если возможность отсутствует или некорректна, изменения не вносятся.

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

ExitListener

Метод addOnExitListener запрашивает сообщение о выходе на responsePort, когда изолят завершается.

Изолят отправит response как сообщение на responsePort в последнюю очередь, перед завершением работы. После отправки сообщения дальнейший код выполняться не будет. Это сообщение можно получить, воспользовавшись методом ReceivePort listen следующим образом:

1ReceivePort responsePort = ReceivePort();
2responsePort.listen((message) {
3  // Сообщение о завершении изолята.
4  print('Isolate has exited.');
5  if (message != null) {
6    print('Exit message: $message');
7  }
8});
9

Если изолят завершился до того, как он смог получить этот запрос, сообщение о завершении отправлено не будет.

Объект response должен соответствовать тем же ограничениям, которые применяются SendPort.send при отправке изоляту из другой изолированной группы: разрешены только простые значения, которые могут быть отправлены всем изолятам, такие как null, логические значения, числа или строки.

Чтобы отменить запрос сообщения о выходе, воспользуйтесь методом removeOnExitListener.


Вот и всё! Давайте коротко вспомним, что мы узнали в этом параграфе:

  • «Тяжёлые» вычисления блокируют основной поток Flutter-приложения. Из-за этого может подтормаживать реакция интерфейса на действия пользователя.

  • Хорошая практика применять параллельные вычисления (вспомните метафору про рыбаков Васю и Петю).

  • Основной инструмент для параллельных вычислений в Dart — это изоляты. Чтобы их создать, можно воспользоваться методом compute, Isolate.run или применить низкоуровневое решение на основе API Dart.

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

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

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф4.5. Advanced изоляты и зоны и асинхронное и параллельное программирование
Следующий параграф5.1. Clean architecture