Saltar al contenido principal

Isolates

Información sobre cómo escribir isolates en Dart.

Esta página analiza algunos ejemplos que utilizan la API de Isolate para implementar isolates.

Deberías usar isolates siempre que tu aplicación maneje cálculos que sean lo suficientemente grandes como para bloquear temporalmente otros cálculos. El ejemplo más común es en aplicaciones de Flutter, cuando necesitas realizar cálculos grandes que de otro modo podrían hacer que la interfaz de usuario deje de responder.

No hay reglas sobre cuándo debes usar isolates, pero aquí hay algunas situaciones más donde pueden ser útiles:

  • Analizar y decodificar JSON blobs excepcionalmente grandes.
  • Procesar y comprimir fotos, audio y video.
  • Convertir archivos de audio y video.
  • Realizar búsquedas y filtrados complejos en listas grandes o dentro de sistemas de archivos.
  • Realizar I/O, como comunicarse con una base de datos.
  • Manejar un gran volumen de solicitudes de red.

Implementar un isolate worker simple

#

Estos ejemplos implementan un isolate principal que inicia (spawns) un isolate worker simple. Isolate.run() simplifica los pasos detrás de la configuración y administración de los isolates worker:

  1. Genera (inicia y crea) un isolate.
  2. Ejecuta una función en el isolate generado.
  3. Captura el resultado.
  4. Devuelve el resultado al isolate principal.
  5. Termina el isolate una vez que el trabajo se ha completado.
  6. Comprueba, captura y lanza excepciones y errores de vuelta al isolate principal.

Ejecutar un método existente en un nuevo isolate

#
  1. Llama a run() para iniciar un nuevo isolate (un worker de fondo), directamente en el isolate principal mientras main() espera el resultado:
dart
const String filename = 'with_keys.json';

void main() async {
  // Read some data.
  final jsonData = await Isolate.run(_readAndParseJson);

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}
  1. Pasa al isolate worker la función que deseas que ejecute como su primer argumento. En este ejemplo, es la función existente _readAndParseJson():
dart
Future<Map<String, dynamic>> _readAndParseJson() async {
  final fileData = await File(filename).readAsString();
  final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
  return jsonData;
}
  1. Isolate.run() toma el resultado que devuelve _readAndParseJson() y envía el valor de vuelta al isolate principal, cerrando el isolate worker.

  2. El isolate worker transfiere la memoria que contiene el resultado al isolate principal. No copia los datos. El isolate worker realiza un paso de verificación para asegurarse de que los objetos tienen permitido ser transferidos.

_readAndParseJson() es una función asíncrona existente que podría ejecutarse con la misma facilidad directamente en el isolate principal. Usar Isolate.run() para ejecutarla en su lugar permite la concurrencia. El isolate worker abstrae por completo los cálculos de _readAndParseJson(). Puede completarse sin bloquear el isolate principal.

El resultado de Isolate.run() es siempre un Future, porque el código en el isolate principal continúa ejecutándose. Que el cálculo que ejecuta el isolate worker sea síncrono o asíncrono no afecta al isolate principal, porque de todos modos se ejecuta de forma concurrente.

Para ver el programa completo, consulta el ejemplo send_and_receive.dart.

Enviar closures con isolates

#

También puedes crear un isolate worker simple con run() usando una función literal, o closure, directamente en el isolate principal.

dart
const String filename = 'with_keys.json';

void main() async {
  // Read some data.
  final jsonData = await Isolate.run(() async {
    final fileData = await File(filename).readAsString();
    final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
    return jsonData;
  });

  // Use that data.
  print('Number of JSON keys: ${jsonData.length}');
}

Este ejemplo logra lo mismo que el anterior. Se inicia un nuevo isolate, calcula algo y envía de vuelta el resultado.

Sin embargo, ahora el isolate envía un closure. Los closures están menos limitados que las funciones nombradas típicas, tanto en cómo funcionan como en cómo se escriben en el código. En este ejemplo, Isolate.run() ejecuta lo que parece ser código local, de forma concurrente. En ese sentido, puedes imaginar que run() funciona como un operador de flujo de control para "ejecutar en paralelo".

Enviar múltiples mensajes entre isolates con puertos

#

Los isolates de corta duración son cómodos de usar, pero requieren una sobrecarga de rendimiento para iniciar nuevos isolates y copiar objetos de un isolate a otro. Si tu código depende de ejecutar repetidamente el mismo cálculo usando Isolate.run, podrías mejorar el rendimiento creando en su lugar isolates de larga duración que no finalicen de inmediato.

Para hacer esto, puedes usar algunas de las APIs de isolates de bajo nivel que Isolate.run abstrae:

Esta sección repasa los pasos necesarios para establecer una comunicación bidireccional entre un isolate recién iniciado y el isolate principal (main isolate). El primer ejemplo, Puertos básicos, presenta el proceso a un nivel general. El segundo ejemplo, Puertos robustos, añade gradualmente funcionalidad más práctica y del mundo real al primero.

ReceivePort y SendPort

#

Configurar una comunicación de larga duración entre isolates requiere dos clases (además de Isolate): ReceivePort y SendPort. Estos puertos son la única forma en que los isolates pueden comunicarse entre sí.

Un ReceivePort es un objeto que maneja mensajes que se envían desde otros isolates. Esos mensajes se envían a través de un SendPort.

Los puertos se comportan de manera similar a los objetos Stream (¡de hecho, los receive ports implementan Stream!) Puedes pensar en un SendPort y un ReceivePort como el StreamController y los listeners de un Stream, respectivamente. Un SendPort es como un StreamController porque les "añades" mensajes con el método SendPort.send(), y esos mensajes son manejados por un listener, en este caso el ReceivePort. Luego, el ReceivePort maneja los mensajes que recibe pasándolos como argumentos a un callback que proporciones.

Configurar puertos

#

Un isolate recién iniciado solo tiene la información que recibe a través de la llamada a Isolate.spawn. Si necesitas que el isolate principal continúe comunicándose con un isolate iniciado más allá de su creación inicial, debes configurar un canal de comunicación donde el isolate iniciado pueda enviar mensajes al isolate principal. Los isolates solo pueden comunicarse mediante el paso de mensajes. No pueden “ver” dentro de la memoria de los demás, de ahí proviene el nombre “isolate” (aislado).

Para configurar esta comunicación bidireccional, primero crea un ReceivePort en el isolate principal, luego pasa su SendPort como argumento al nuevo isolate al iniciarlo con Isolate.spawn. El nuevo isolate crea entonces su propio ReceivePort y envía su SendPort de vuelta a través del SendPort que le pasó el isolate principal. El isolate principal recibe este SendPort, y ahora ambas partes tienen un canal abierto para enviar y recibir mensajes.

Una figura que muestra eventos siendo introducidos, uno a uno, en el bucle de eventos

  1. Crea un ReceivePort en el isolate principal. El SendPort se crea automáticamente como una propiedad en el ReceivePort.
  2. Inicia el isolate worker con Isolate.spawn()
  3. Pasa una referencia a ReceivePort.sendPort como el primer mensaje al isolate worker.
  4. Crea otro nuevo ReceivePort en el isolate worker.
  5. Pasa una referencia al ReceivePort.sendPort del isolate worker como el primer mensaje de vuelta al isolate principal.

Junto con la creación de los puertos y la configuración de la comunicación, también deberás indicarle a los puertos qué hacer cuando reciban mensajes. Esto se hace usando el método listen en cada respectivo ReceivePort.

Una figura que muestra eventos siendo introducidos, uno a uno, en el bucle de eventos

  1. Envía un mensaje a través de la referencia del isolate principal al SendPort del isolate worker.
  2. Recibe y maneja el mensaje a través de un listener en el ReceivePort del isolate worker. Aquí es donde se ejecuta el cálculo que deseas mover fuera del isolate principal.
  3. Envía un mensaje de retorno a través de la referencia del isolate worker al SendPort del isolate principal.
  4. Recibe el mensaje a través de un listener en el ReceivePort del isolate principal.

Ejemplo básico de puertos

#

Este ejemplo demuestra cómo configurar un isolate worker de larga duración con comunicación bidireccional entre este y el isolate principal. El código utiliza el ejemplo de enviar texto JSON a un nuevo isolate, onde el JSON se analizará y decodificará, antes de ser enviado de vuelta al isolate principal.

Paso 1: Definir la clase worker

#

Primero, crea una clase para tu isolate worker en segundo plano. Esta clase contiene toda la funcionalidad que necesitas para:

  • Generar un isolate.
  • Enviar mensajes a ese isolate.
  • Hacer que el isolate decodifique un JSON.
  • Enviar el JSON decodificado de vuelta al isolate principal.

La clase expone dos métodos públicos: uno que inicia el isolate worker y otro que maneja el envío de mensajes a ese isolate worker.

Las secciones restantes de este ejemplo te mostrarán cómo completar los métodos de la clase, uno por uno.

dart
class Worker {
  Future<void> spawn() async {
    // TODO: Add functionality to spawn a worker isolate.
  }

  void _handleResponsesFromIsolate(dynamic message) {
    // TODO: Handle messages sent back from the worker isolate.
  }

  static void _startRemoteIsolate(SendPort port) {
    // TODO: Define code that should be executed on the worker isolate.
  }

  Future<void> parseJson(String message) async {
    // TODO: Define a public method that can
    // be used to send messages to the worker isolate.
  }
}

Paso 2: Generar un isolate worker

#

El método Worker.spawn es donde agruparás el código para crear el isolate worker y asegurarte de que pueda recibir y enviar mensajes.

  • Primero, crea un ReceivePort. Esto permite que el isolate principal reciba mensajes enviados desde el isolate worker recién iniciado.
  • A continuación, añade un listener al receive port para manejar los mensajes que el isolate worker enviará de vuelta. El callback pasado al listener, _handleResponsesFromIsolate, se cubrirá en el paso 4.
  • Por último, inicia el isolate worker con Isolate.spawn. Espera dos argumentos: una función a ejecutar en el isolate worker (cubierto en el paso 3), y la propiedad sendPort del receive port.
dart
Future<void> spawn() async {
  final receivePort = ReceivePort();
  receivePort.listen(_handleResponsesFromIsolate);
  await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}

El argumento receivePort.sendPort se pasará al callback (_startRemoteIsolate) como argumento cuando se llame en el isolate worker. Este es el primer paso para asegurar que el isolate worker tenga una forma de enviar mensajes de vuelta al isolate principal.

Paso 3: Ejecutar código en el isolate worker

#

En este paso, defines el método _startRemoteIsolate que se envía al isolate worker para ejecutarse cuando se inicia. Este método es como el método “main” para el isolate worker.

  • Primero, crea otro nuevo ReceivePort. Este puerto recibe mensajes futuros del isolate principal.
  • A continuación, envía el SendPort de ese puerto de vuelta al isolate principal.
  • Por último, añade un listener al nuevo ReceivePort. Este listener maneja mensajes que el isolate principal envía al isolate worker.
dart
static void _startRemoteIsolate(SendPort port) {
  final receivePort = ReceivePort();
  port.send(receivePort.sendPort);

  receivePort.listen((dynamic message) async {
    if (message is String) {
      final transformed = jsonDecode(message);
      port.send(transformed);
    }
  });
}

El listener en el ReceivePort del worker decodifica el JSON pasado desde el isolate principal y luego envía el JSON decodificado de vuelta al isolate principal.

Este listener es el punto de entrada para los mensajes enviados desde el isolate principal al isolate worker. Esta es la única oportunidad que tienes de indicarle al isolate worker qué código ejecutar en el futuro.

Paso 4: Manejar mensajes en el isolate principal

#

Por último, debes indicarle al isolate principal cómo manejar los mensajes enviados desde el isolate worker de vuelta al isolate principal. Para hacerlo, debes completar el método _handleResponsesFromIsolate. Recuerda que este método se pasa al método receivePort.listen, como se describió en el paso 2:

dart
Future<void> spawn() async {
  final receivePort = ReceivePort();
  receivePort.listen(_handleResponsesFromIsolate);
  await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}

Recuerda también que enviaste un SendPort de vuelta al isolate principal en el paso 3. Este método maneja la recepción de ese SendPort, así como el manejo de futuros mensajes (que serán JSON decodificados).

  • Primero, verifica si el mensaje es un SendPort. Si es así, asigna ese puerto a la propiedad _sendPort de la clase para que pueda usarse para enviar mensajes más tarde.
  • A continuación, verifica si el mensaje es del tipo Map<String, dynamic>, el tipo esperado de JSON decodificado. Si es así, maneja ese mensaje con la lógica específica de tu aplicación. En este ejemplo, se imprime el mensaje.
dart
void _handleResponsesFromIsolate(dynamic message) {
  if (message is SendPort) {
    _sendPort = message;
    _isolateReady.complete();
  } else if (message is Map<String, dynamic>) {
    print(message);
  }
}

Paso 5: Agregar un completer para asegurar que tu isolate esté configurado

#

Para completar la clase, define un método público llamado parseJson, el cual se encarga de enviar mensajes al isolate worker. También debe asegurar que los mensajes puedan ser enviados antes de que el isolate esté completamente configurado. Para manejar esto, usa un Completer.

  • Primero, añade una propiedad a nivel de clase llamada Completer y nómbrala _isolateReady.
  • A continuación, añade una llamada a complete() en el completer en el método _handleResponsesFromIsolate (creado en el paso 4) si el mensaje es un SendPort.
  • Por último, en el método parseJson, añade await _isolateReady.future antes de añadir _sendPort.send. Esto asegura que no se pueda enviar ningún mensaje al isolate worker hasta que se haya iniciado y haya enviado su SendPort de vuelta al isolate principal.
dart
Future<void> parseJson(String message) async {
  await _isolateReady.future;
  _sendPort.send(message);
}

Ejemplo completo

#
Expandir para ver el ejemplo completo
dart
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

void main() async {
  final worker = Worker();
  await worker.spawn();
  await worker.parseJson('{"key":"value"}');
}

class Worker {
  late SendPort _sendPort;
  final Completer<void> _isolateReady = Completer.sync();

  Future<void> spawn() async {
    final receivePort = ReceivePort();
    receivePort.listen(_handleResponsesFromIsolate);
    await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
  }

  void _handleResponsesFromIsolate(dynamic message) {
    if (message is SendPort) {
      _sendPort = message;
      _isolateReady.complete();
    } else if (message is Map<String, dynamic>) {
      print(message);
    }
  }

  static void _startRemoteIsolate(SendPort port) {
    final receivePort = ReceivePort();
    port.send(receivePort.sendPort);

    receivePort.listen((dynamic message) async {
      if (message is String) {
        final transformed = jsonDecode(message);
        port.send(transformed);
      }
    });
  }

  Future<void> parseJson(String message) async {
    await _isolateReady.future;
    _sendPort.send(message);
  }

}

Ejemplo robusto de puertos

#

El ejemplo anterior explicó los bloques de construcción básicos necesarios para configurar un isolate de larga duración con comunicación bidireccional. Como se mencionó, ese ejemplo carece de algunas características importantes, como el manejo de errores, la capacidad de cerrar los puertos cuando ya no están en uso y las inconsistencias en torno al orden de los mensajes en algunas situaciones.

Este ejemplo amplía la información del primer ejemplo al crear un isolate worker de larga duración que cuenta con estas características adicionales y más, y sigue mejores patrones de diseño. Aunque este código tiene similitudes con el primer ejemplo, no es una extensión de ese ejemplo.

Paso 1: Definir la clase worker

#

Primero, crea una clase para tu isolate worker en segundo plano. Esta clase contiene toda la funcionalidad que necesitas para:

  • Generar un isolate.
  • Enviar mensajes a ese isolate.
  • Hacer que el isolate decodifique un JSON.
  • Enviar el JSON decodificado de vuelta al isolate principal.

La clase expone tres métodos públicos: uno que crea el isolate worker, uno que maneja el envío de mensajes a ese isolate worker, y uno que puede cerrar los puertos cuando ya no están en uso.

dart
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;

  Future<Object?> parseJson(String message) async {
    // TODO: Ensure the port is still open.
    _commands.send(message);
  }

  static Future<Worker> spawn() async {
    // TODO: Add functionality to create a new Worker object with a
    //  connection to a spawned isolate.
    throw UnimplementedError();
  }

  Worker._(this._responses, this._commands) {
    // TODO: Initialize main isolate receive port listener.
  }

  void _handleResponsesFromIsolate(dynamic message) {
    // TODO: Handle messages sent back from the worker isolate.
  }

  static void _handleCommandsToIsolate(ReceivePort rp, SendPort sp) async {
    // TODO: Handle messages sent back from the worker isolate.
  }

  static void _startRemoteIsolate(SendPort sp) {
    // TODO: Initialize worker isolate's ports.
  }
}

Paso 2: Crear un RawReceivePort en el método Worker.spawn

#

Antes de iniciar un isolate, debes crear un RawReceivePort, que es un ReceivePort de nivel inferior. Usar RawReceivePort es un patrón preferido porque te permite separar la lógica de inicio de tu isolate de la lógica que maneja el paso de mensajes en el isolate.

En el método Worker.spawn:

  • Primero, crea el RawReceivePort. Este ReceivePort solo es responsable de recibir el mensaje inicial del isolate worker, que será un SendPort.
  • A continuación, crea un Completer que indicará cuándo el isolate está listo para recibir mensajes. Cuando este se complete, devolverá un record con un ReceivePort y un SendPort.
  • A continuación, define la propiedad RawReceivePort.handler. Esta propiedad es un Function? que se comporta como ReceivePort.listener. Se llama a la función cuando este puerto recibe un mensaje.
  • Dentro de la función handler, llama a connection.complete(). Este método espera un record con un ReceivePort y un SendPort como argumento. El SendPort es el mensaje inicial enviado desde el isolate worker, el cual se asignará en el siguiente paso al SendPort a nivel de clase llamado _commands.
  • Luego, crea un nuevo ReceivePort con el constructor ReceivePort.fromRawReceivePort y pasa el initPort.
dart
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;

  static Future<Worker> spawn() async {
    // Create a receive port and add its initial message handler.
    final initPort = RawReceivePort();
    final connection = Completer<(ReceivePort, SendPort)>.sync();
    initPort.handler = (initialMessage) {
      final commandPort = initialMessage as SendPort;
      connection.complete((
        ReceivePort.fromRawReceivePort(initPort),
        commandPort,
      ));
    };
  }
}

Al crear un RawReceivePort primero y luego un ReceivePort, podrás añadir un nuevo callback a ReceivePort.listen más adelante. Por el contrario, si crearas un ReceivePort de inmediato, solo podrías añadir un listener, porque ReceivePort implementa Stream, en lugar de BroadcastStream.

De manera efectiva, esto te permite separar la lógica de inicio del isolate de la lógica que maneja la recepción de mensajes después de que se completa la configuración de la comunicación. Este beneficio será más evidente a medida que crezca la lógica en los demás métodos.

Paso 3: Iniciar un isolate worker con Isolate.spawn

#

Este paso continúa completando el método Worker.spawn. Añadirás el código necesario para iniciar un isolate y devolver una instancia de Worker de esta clase. En este ejemplo, la llamada a Isolate.spawn está envuelta en un bloque try/catch, lo que asegura que, si el isolate no se inicia, el initPort se cerrará y el objeto Worker no se creará.

  • Primero, intenta iniciar un isolate worker en un bloque try/catch. Si el inicio del isolate worker falla, cierra el receive port creado en el paso anterior. El método pasado a Isolate.spawn se cubrirá en un paso posterior.
  • A continuación, espera al connection.future y desestructura el send port y receive port del record que devuelve.
  • Por último, devuelve una instancia de Worker llamando a su constructor privado y pasando los puertos de ese completer.
dart
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;

  static Future<Worker> spawn() async {
    // Create a receive port and add its initial message handler.
    final initPort = RawReceivePort();
    final connection = Completer<(ReceivePort, SendPort)>.sync();
    initPort.handler = (initialMessage) {
      final commandPort = initialMessage as SendPort;
      connection.complete((
        ReceivePort.fromRawReceivePort(initPort),
        commandPort,
      ));
    };
    // Spawn the isolate.
    try {
      await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
    } on Object {
      initPort.close();
      rethrow;
    }

    final (ReceivePort receivePort, SendPort sendPort) =
        await connection.future;

    return Worker._(receivePort, sendPort);
  }
}

Ten en cuenta que en este ejemplo (en comparación con el ejemplo anterior), Worker.spawn actúa como un constructor estático asíncrono para esta clase y es la única forma de crear una instancia de Worker. Esto simplifica la API, haciendo que el código que crea una instancia de Worker sea más limpio.

Paso 4: Completar el proceso de configuración del isolate

#

En este paso, completarás el proceso básico de configuración del isolate. Esto se correlaciona casi por completo con el ejemplo anterior y no hay conceptos nuevos. Hay un ligero cambio en el hecho de que el código se divide en más métodos, lo cual es una práctica de diseño que te prepara para añadir más funcionalidad durante el resto de este ejemplo. Para obtener una guía detallada del proceso básico de configuración de un isolate, consulta el ejemplo básico de puertos.

Primero, crea el constructor privado que devuelve el método Worker.spawn el constructor. En el cuerpo del constructor, añade un listener al receive port utilizado por el isolate principal y pasa a ese listener un método aún no definido llamado _handleResponsesFromIsolate.

dart
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;

  Worker._(this._responses, this._commands) {
    _responses.listen(_handleResponsesFromIsolate);
  }
}

A continuación, añade el código a _startRemoteIsolate que se encarga de inicializar los puertos en el isolate worker. Recuerda que este método se pasó a Isolate.spawn en el método Worker.spawn y se le pasará el SendPort del isolate principal como argumento.

  • Crea un nuevo ReceivePort.
  • Envía el SendPort de ese puerto de vuelta al isolate principal.
  • Llama a un nuevo método llamado _handleCommandsToIsolate y pasa como argumentos tanto el nuevo ReceivePort como el SendPort del isolate principal.
dart
static void _startRemoteIsolate(SendPort sendPort) {
  final receivePort = ReceivePort();
  sendPort.send(receivePort.sendPort);
  _handleCommandsToIsolate(receivePort, sendPort);
}

A continuación, añade el método _handleCommandsToIsolate, el cual se encarga de recibir mensajes del isolate principal, decodificar JSON en el isolate worker y enviar el JSON decodificado de vuelta como respuesta.

  • Primero, declara un listener en el ReceivePort del isolate worker.
  • Dentro del callback añadido al listener, intenta decodificar el JSON pasado desde el isolate principal dentro de un bloque try/catch. Si la decodificación tiene éxito, envía el JSON decodificado de vuelta al isolate principal.
  • Si hay un error, envía de vuelta un RemoteError.
dart
static void _handleCommandsToIsolate(
  ReceivePort receivePort,
  SendPort sendPort,
) {
  receivePort.listen((message) {
    try {
      final jsonData = jsonDecode(message as String);
      sendPort.send(jsonData);
    } catch (e) {
      sendPort.send(RemoteError(e.toString(), ''));
    }
  });
}

A continuación, añade el código para el método _handleResponsesFromIsolate.

  • Primero, verifica si el mensaje es un RemoteError, en cuyo caso deberías lanzar (throw) ese error.
  • De lo contrario, imprime el mensaje. En pasos futuros, actualizarás este código para devolver mensajes en lugar de imprimirlos.
dart
void _handleResponsesFromIsolate(dynamic message) {
  if (message is RemoteError) {
    throw message;
  } else {
    print(message);
  }
}

Por último, añade el método parseJson, el cual es un método público que permite al código externo enviar JSON al isolate worker para ser decodificado.

dart
Future<Object?> parseJson(String message) async {
  _commands.send(message);
}

Actualizarás este método en el siguiente paso.

Paso 5: Manejar múltiples mensajes al mismo tiempo

#

Actualmente, si envías mensajes rápidamente al isolate worker, el isolate enviará la respuesta JSON decodificada en el orden en que se completen, en lugar del orden en que se enviaron. No tienes forma de determinar qué respuesta corresponde a qué mensaje.

En este paso, solucionarás este problema asignando un id a cada mensaje y utilizando objetos Completer para asegurar que cuando el código externo llame a parseJson la respuesta que se devuelve a ese llamador sea la respuesta correcta.

Primero, añade dos propiedades a nivel de clase a Worker:

  • Map<int, Completer<Object?>> _activeRequests
  • int _idCounter
dart
class Worker {
  final SendPort _commands;
  final ReceivePort _responses;
  final Map<int, Completer<Object?>> _activeRequests = {};
  int _idCounter = 0;
  // ···
}

El mapa _activeRequests asocia un mensaje enviado al isolate worker con un Completer. Las claves utilizadas en _activeRequests se toman de _idCounter, el cual se incrementará a medida que se envíen más mensajes.

A continuación, actualiza el método parseJson para crear completers antes de enviar mensajes al isolate worker.

  • Primero, crea un Completer.
  • A continuación, incrementa _idCounter, de modo que cada Completer se asocie con un número único.
  • Añade una entrada al mapa _activeRequests en la que la clave sea el número actual de _idCounter y el completer sea el valor.
  • Envía el mensaje al isolate worker junto con el id. Debido a que solo puedes enviar un valor a través de la SendPort, envuelve el id y el mensaje en un record.
  • Por último, devuelve el future del completer, que eventualmente contendrá la respuesta del isolate worker.
dart
Future<Object?> parseJson(String message) async {
  final completer = Completer<Object?>.sync();
  final id = _idCounter++;
  _activeRequests[id] = completer;
  _commands.send((id, message));
  return await completer.future;
}

También necesitas actualizar _handleResponsesFromIsolate y _handleCommandsToIsolate para manejar este sistema.

En _handleCommandsToIsolate, debes tener en cuenta que el mensaje (message) es un record con dos valores, en lugar de solo el texto JSON. Hazlo desestructurando los valores de message.

Luego, después de decodificar el JSON, actualiza la llamada a sendPort.send para pasar tanto el id como el JSON decodificado de vuelta al isolate principal, de nuevo usando un record.

dart
static void _handleCommandsToIsolate(
  ReceivePort receivePort,
  SendPort sendPort,
) {
  receivePort.listen((message) {
    final (int id, String jsonText) = message as (int, String); // New
    try {
      final jsonData = jsonDecode(jsonText);
      sendPort.send((id, jsonData)); // Updated
    } catch (e) {
      sendPort.send((id, RemoteError(e.toString(), '')));
    }
  });
}

Por último, actualiza _handleResponsesFromIsolate.

  • Primero, vuelve a desestructurar el id y la respuesta del argumento del mensaje.
  • Luego, remueve el completer correspondiente a esta solicitud del mapa _activeRequests.
  • Por último, en lugar de lanzar un error o imprimir el JSON decodificado, completa el completer pasando la respuesta. Cuando este se complete, la respuesta se devolverá al código que llamó a parseJson en el isolate principal.
dart
void _handleResponsesFromIsolate(dynamic message) {
  final (int id, Object? response) = message as (int, Object?); // New
  final completer = _activeRequests.remove(id)!; // New

  if (response is RemoteError) {
    completer.completeError(response); // Updated
  } else {
    completer.complete(response); // Updated
  }
}

Paso 6: Agregar funcionalidad para cerrar los puertos

#

Cuando tu código ya no utilice el isolate, debes cerrar los puertos en el isolate principal y en el isolate worker.

  • Primero, agrega un booleano a nivel de clase que rastree si los puertos están cerrados.
  • Then, add the Worker.close method. Within this method:
    • Actualiza _closed a true.
    • Envía un mensaje final al isolate worker. Este mensaje es una String que dice “shutdown”, pero podría ser cualquier objeto que desees. Lo usarás en el siguiente fragmento de código.
  • Por último, verifica si _activeRequests está vacío. Si lo está, cierra el ReceivePort del isolate principal llamado _responses.
dart
class Worker {
  bool _closed = false;
  // ···
  void close() {
    if (!_closed) {
      _closed = true;
      _commands.send('shutdown');
      if (_activeRequests.isEmpty) _responses.close();
      print('--- port closed --- ');
    }
  }
}
  • A continuación, debes manejar el mensaje de apagado (“shutdown”) en el isolate worker. Añade el siguiente código al método _handleCommandsToIsolate. Este código verificará si el mensaje es una String que dice “shutdown”. Si lo es, cerrará el ReceivePort del isolate worker y retornará.
dart
static void _handleCommandsToIsolate(
  ReceivePort receivePort,
  SendPort sendPort,
) {
  receivePort.listen((message) {
    // New if-block.
    if (message == 'shutdown') {
      receivePort.close();
      return;
    }
    final (int id, String jsonText) = message as (int, String);
    try {
      final jsonData = jsonDecode(jsonText);
      sendPort.send((id, jsonData));
    } catch (e) {
      sendPort.send((id, RemoteError(e.toString(), '')));
    }
  });
}
  • Por último, debes añadir código para verificar si los puertos están cerrados antes de intentar enviar mensajes. Añade una línea en el método Worker.parseJson.
dart
Future<Object?> parseJson(String message) async {
  if (_closed) throw StateError('Closed'); // New
  final completer = Completer<Object?>.sync();
  final id = _idCounter++;
  _activeRequests[id] = completer;
  _commands.send((id, message));
  return await completer.future;
}

Ejemplo completo

#
Expande aquí para ver el ejemplo completo
dart
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';

void main() async {
  final worker = await Worker.spawn();
  print(await worker.parseJson('{"key":"value"}'));
  print(await worker.parseJson('"banana"'));
  print(await worker.parseJson('[true, false, null, 1, "string"]'));
  print(
    await Future.wait([worker.parseJson('"yes"'), worker.parseJson('"no"')]),
  );
  worker.close();
}

class Worker {
  final SendPort _commands;
  final ReceivePort _responses;
  final Map<int, Completer<Object?>> _activeRequests = {};
  int _idCounter = 0;
  bool _closed = false;

  Future<Object?> parseJson(String message) async {
    if (_closed) throw StateError('Closed');
    final completer = Completer<Object?>.sync();
    final id = _idCounter++;
    _activeRequests[id] = completer;
    _commands.send((id, message));
    return await completer.future;
  }

  static Future<Worker> spawn() async {
    // Create a receive port and add its initial message handler.
    final initPort = RawReceivePort();
    final connection = Completer<(ReceivePort, SendPort)>.sync();
    initPort.handler = (initialMessage) {
      final commandPort = initialMessage as SendPort;
      connection.complete((
        ReceivePort.fromRawReceivePort(initPort),
        commandPort,
      ));
    };

    // Spawn the isolate.
    try {
      await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
    } on Object {
      initPort.close();
      rethrow;
    }

    final (ReceivePort receivePort, SendPort sendPort) =
        await connection.future;

    return Worker._(receivePort, sendPort);
  }

  Worker._(this._responses, this._commands) {
    _responses.listen(_handleResponsesFromIsolate);
  }

  void _handleResponsesFromIsolate(dynamic message) {
    final (int id, Object? response) = message as (int, Object?);
    final completer = _activeRequests.remove(id)!;

    if (response is RemoteError) {
      completer.completeError(response);
    } else {
      completer.complete(response);
    }

    if (_closed && _activeRequests.isEmpty) _responses.close();
  }

  static void _handleCommandsToIsolate(
    ReceivePort receivePort,
    SendPort sendPort,
  ) {
    receivePort.listen((message) {
      if (message == 'shutdown') {
        receivePort.close();
        return;
      }
      final (int id, String jsonText) = message as (int, String);
      try {
        final jsonData = jsonDecode(jsonText);
        sendPort.send((id, jsonData));
      } catch (e) {
        sendPort.send((id, RemoteError(e.toString(), '')));
      }
    });
  }

  static void _startRemoteIsolate(SendPort sendPort) {
    final receivePort = ReceivePort();
    sendPort.send(receivePort.sendPort);
    _handleCommandsToIsolate(receivePort, sendPort);
  }

  void close() {
    if (!_closed) {
      _closed = true;
      _commands.send('shutdown');
      if (_activeRequests.isEmpty) _responses.close();
      print('--- port closed --- ');
    }
  }
}