Isolate.run [disponible desde Dart 2.19] es una API de alto nivel que simplifica la ejecución de tareas intensivas en un hilo de ejecución separado, devolviendo el resultado directamente como un Future. En el modelo de concurrencia de Dart, cada Isolate tiene su propio heap de memoria y no comparte estado con otros; la comunicación entre ellos se realiza estrictamente mediante el paso de mensajes (copying/transferring).
Tradicionalmente, para ejecutar código en paralelo con Isolate.spawn, debías gestionar manualmente la infraestructura de comunicación: crear un ReceivePort en el isolate padre para escuchar mensajes y un SendPort en el isolate hijo para responder. Esto introducía un boilerplate considerable y una complejidad innecesaria si el objetivo era simplemente realizar un cálculo y obtener un valor. Isolate.run abstrae toda esa gestión de puertos, creando un canal de comunicación efímero que se cierra automáticamente una vez que la función termina.
Debes usar Isolate.run cuando tengas una tarea computacionalmente costosa (como el parsing de un JSON masivo o procesamiento de imágenes) que quieres ejecutar sin bloquear el event loop principal y cuyo resultado necesitas de vuelta en el hilo original. Sin embargo, si tu arquitectura requiere un flujo continuo de datos (por ejemplo, un stream de telemetría que llega cada milisegundo), Isolate.run no es la herramienta adecuada; en ese caso, necesitas Isolate.spawn para mantener un canal de comunicación abierto. Si intentas usarlo para tareas triviales, el overhead de crear y destruir el isolate será mayor que el tiempo ahorrado en la computación, degradando el rendimiento. Finalmente, ten cuidado con lo que intentas pasar: los objetos enviados al isolate se copian, por lo que cualquier cambio realizado dentro de la función no afectará al objeto original en el isolate principal.
import 'dart:isolate';
// La función debe ser top-level o estática para que el runtime
// pueda instanciarla en el nuevo isolate sin llevarse el contexto actual.
int procesarChecksumMasivo(List<int> datos) {
int checksum = 0;
// Simulamos una carga de trabajo intensiva sobre un dataset grande
for (var i = 0; i < datos.length; i++) {
checksum ^= datos[i];
}
return checksum;
}
void main() async {
// Generamos una lista de 10 millones de enteros para la prueba
final datosPesados = List<int>.generate(10_000_000, (i) => i % 256);
print('Iniciando computación en un isolate separado...');
final reloj = Stopwatch().start();
try {
// Isolate.run encapsula el spawn, la creación de puertos
// y la resolución del Future con el valor de retorno.
final resultado = await Isolate.run(() => procesarChecksumMasivo(datosPesados));
reloj.stop();
print('Checksum calculado: $resultado');
print('Tiempo de ejecución: ${re reloj.elapsed.inMilliseconds}ms');
} catch (e) {
print('Error en el isolate: $e');
}
}
En el ejemplo anterior, procesarChecksumMasivo es una función top-level, lo cual es crucial porque permite que el VM de Dart cargue la función en el nuevo isolate sin necesidad de arrastrar el estado de la función main. Cuando llamamos a Isolate.run, el runtime no envía la lista datosPesados por referencia; en su lugar, realiza una copia de los datos hacia el heap del nuevo isolate. Aunque esto implica un costo de memoria, garantiza la integridad del modelo de aislamiento de Dart.
La magia ocurre en la gestión del Future. Isolate.run crea internamente un ReceivePort, lo pasa al nuevo isolate de forma implícita, y se queda esperando el mensaje. Una vez que procesarChecksumMasivo retorna el int, el runtime envía ese valor de vuelta al isolate principal, el cual resuelve el Future que estamos esperando con el await.
El uso de Stopwatch nos permite observar que, aunque hay un pequeño delay inicial por la creación del isolate, el procesamiento de los 10 millones de elementos no bloquea la capacidad de respuesta del hilo principal, permitiendo que otras tareas asíncronas continúen mientras tanto.
El error frecuente
Un error muy común al migrar de funciones simples a Isolate.run es intentar pasar un método de instancia en lugar de un método estático o una función top-level.
class Procesador {
int factor = 2;
// ERROR: Este método no es estático y captura el estado de 'this'
int calcular(List<int> lista) {
return lista.map((e) => e * factor).reduce((a, b) => a + b);
}
}
void main() async {
final p = Procesador();
final datos = [1, 2, 3];
// Esto fallará en tiempo de ejecución o impedirá la compilación
// porque el runtime no puede "enviar" la instancia de 'p'
// (el contexto 'this') de forma segura para ejecutar el método.
await Isolate.run(() => p.calcular(datos));
}
Cuando pasas un método de instancia, estás intentando capturar implicitamente la instancia de la clase (this). Para evitar este error, define siempre la lógica pesada en una función independiente o declara el método como static.
N° 73