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:
- Genera (inicia y crea) un isolate.
- Ejecuta una función en el isolate generado.
- Captura el resultado.
- Devuelve el resultado al isolate principal.
- Termina el isolate una vez que el trabajo se ha completado.
- Comprueba, captura y lanza excepciones y errores de vuelta al isolate principal.
Ejecutar un método existente en un nuevo isolate
#- Llama a
run()para iniciar un nuevo isolate (un worker de fondo), directamente en el isolate principal mientrasmain()espera el resultado:
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}');
}
- Pasa al isolate worker la función que deseas que ejecute
como su primer argumento. En este ejemplo, es la función existente
_readAndParseJson():
Future<Map<String, dynamic>> _readAndParseJson() async {
final fileData = await File(filename).readAsString();
final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
return jsonData;
}
-
Isolate.run()toma el resultado que devuelve_readAndParseJson()y envía el valor de vuelta al isolate principal, cerrando el isolate worker. -
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.
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.
- Crea un
ReceivePorten el isolate principal. ElSendPortse crea automáticamente como una propiedad en elReceivePort. - Inicia el isolate worker con
Isolate.spawn() - Pasa una referencia a
ReceivePort.sendPortcomo el primer mensaje al isolate worker. - Crea otro nuevo
ReceivePorten el isolate worker. - Pasa una referencia al
ReceivePort.sendPortdel 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.
- Envía un mensaje a través de la referencia del isolate principal al
SendPortdel isolate worker. - Recibe y maneja el mensaje a través de un listener en el
ReceivePortdel isolate worker. Aquí es donde se ejecuta el cálculo que deseas mover fuera del isolate principal. - Envía un mensaje de retorno a través de la referencia del isolate worker al
SendPortdel isolate principal. - Recibe el mensaje a través de un listener en el
ReceivePortdel 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.
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 propiedadsendPortdel receive port.
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
SendPortde 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.
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:
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_sendPortde 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.
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
Completery 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 unSendPort. - Por último, en el método
parseJson, añadeawait _isolateReady.futureantes 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 suSendPortde vuelta al isolate principal.
Future<void> parseJson(String message) async {
await _isolateReady.future;
_sendPort.send(message);
}
Ejemplo completo
#Expandir para ver el ejemplo completo
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.
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. EsteReceivePortsolo es responsable de recibir el mensaje inicial del isolate worker, que será unSendPort. - A continuación, crea un
Completerque indicará cuándo el isolate está listo para recibir mensajes. Cuando este se complete, devolverá un record con unReceivePorty unSendPort. - A continuación, define la propiedad
RawReceivePort.handler. Esta propiedad es unFunction?que se comporta comoReceivePort.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 unReceivePorty unSendPortcomo argumento. ElSendPortes el mensaje inicial enviado desde el isolate worker, el cual se asignará en el siguiente paso alSendPorta nivel de clase llamado_commands. - Luego, crea un nuevo
ReceivePortcon el constructorReceivePort.fromRawReceivePorty pasa elinitPort.
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 aIsolate.spawnse cubrirá en un paso posterior. - A continuación, espera al
connection.futurey desestructura el send port y receive port del record que devuelve. - Por último, devuelve una instancia de
Workerllamando a su constructor privado y pasando los puertos de ese completer.
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.
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
SendPortde ese puerto de vuelta al isolate principal. - Llama a un nuevo método llamado
_handleCommandsToIsolatey pasa como argumentos tanto el nuevoReceivePortcomo elSendPortdel isolate principal.
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
ReceivePortdel 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.
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íaslanzar (throw)ese error. - De lo contrario, imprime el mensaje. En pasos futuros, actualizarás este código para devolver mensajes en lugar de imprimirlos.
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.
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?>> _activeRequestsint _idCounter
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 cadaCompleterse asocie con un número único. - Añade una entrada al mapa
_activeRequestsen la que la clave sea el número actual de_idCountery 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.
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.
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
parseJsonen el isolate principal.
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.closemethod. Within this method:- Actualiza
_closeda true. - Envía un mensaje final al isolate worker.
Este mensaje es una
Stringque dice “shutdown”, pero podría ser cualquier objeto que desees. Lo usarás en el siguiente fragmento de código.
- Actualiza
- Por último, verifica si
_activeRequestsestá vacío. Si lo está, cierra elReceivePortdel isolate principal llamado_responses.
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 unaStringque dice “shutdown”. Si lo es, cerrará elReceivePortdel isolate worker y retornará.
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.
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
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 --- ');
}
}
}
A menos que se indique lo contrario, la documentación en este sitio refleja Dart 3.12.2. Página actualizada por última vez el 2025-12-10. Ver fuente oreportar un problema.