Dart concurrencia e Isolate
La concurrencia es la capacidad de manejar múltiples tareas al mismo tiempo.
El modelo de concurrencia de Dart es diferente del multihilo tradicional; utiliza Isolate (zonas aisladas) para lograr computación paralela.
Este capítulo presenta el modelo de concurrencia de Dart, el concepto y uso de Isolate, y el mecanismo de paso de mensajes.
Modelo de concurrencia de Dart
La mayoría de los lenguajes de programación utilizan un modelo de múltiples hilos con memoria compartida, donde varios hilos comparten el mismo espacio de memoria.
La desventaja de este modelo es que es propenso a condiciones de carrera (race condition) y bloqueos mutuos (deadlock).
Dart adopta una estrategia diferente:Cada Isolate tiene su propio montón de memoria independientey los Isolate no comparten memoria entre sí.
Se comunican mediante el paso de mensajes, lo que evita fundamentalmente el problema de la competencia de datos.
El modelo de concurrencia de Dart tiene tres niveles:
| Nivel | Mecanismo | Escenario de aplicación |
|---|---|---|
| Bucle de eventos (Event Loop) | Asíncrono de un solo hilo (Event Loop) | Operaciones de E/S, temporizadores, interacción del usuario |
| Isolate | Memoria independiente + paso de mensajes | Cálculos intensivos en CPU |
| Future/Stream | Sintaxis de programación asíncrona | La mayoría de los escenarios de desarrollo diario |
La mayoría de las necesidades de concurrencia de los programas Dart pueden satisfacerse con async/await y Future/Stream. Isolate solo es necesario cuando necesitas realizar una gran cantidad de cálculos intensivos en CPU. Introducir Isolate prematuramente complica el código sin grandes beneficios.
Concepto y uso de Isolate
Isolate es la unidad de concurrencia de Dart; cada Isolate tiene su propia memoria independiente y su propio bucle de eventos.
El programa principal en sí mismo se ejecuta en un Isolate (el Isolate principal).
Crear un nuevo Isolate con Isolate.spawn
Ejemplo
// Esta función se ejecutará en el nuevo Isolate
// SendPort se utiliza para enviar mensajes al Isolate principal
void heavyComputation(SendPort sendPort) {
print('El nuevo Isolate comienza a calcular...');
// Simula un cálculo intensivo en CPU
int sum = 0;
for (int i = 1; i <= 10000000; i++) {
sum += i;
}
// Envía el resultado del cálculo de vuelta al Isolate principal
sendPort.send(sum);
print('El nuevo Isolate terminó de calcular, el resultado ha sido enviado');
}
Future<void> main() async {
print('El Isolate principal se inicia');
// Se crea un ReceivePort para recibir mensajes
var receivePort = ReceivePort();
// spawn crea un nuevo Isolate
await Isolate.spawn(heavyComputation, receivePort.sendPort);
print('El Isolate principal puede hacer otras cosas mientras espera el resultado...');
// Espera el resultado del cálculo del nuevo Isolate
var result = await receivePort.first;
print('Resultado EJEMPLO: suma de 1 a 10000000 = $result');
receivePort.close();
print('El Isolate principal termina');
}
主 Isolate 启动 主 Isolate 在等待结果的同时可以做其他事情... 新 Isolate 开始计算... 新 Isolate 计算完成,结果已发送 EJEMPLO 计算结果: 1 到 10000000 的和 = 50000005000000 主 Isolate 结束
Comparación: diferencia de rendimiento con y sin Isolate
Ejemplo
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
void main() {
// Calcular en el Isolate principal: bloqueará todas las demás operaciones
var startTime = DateTime.now();
// Nota: si n es demasiado grande, este cálculo consumirá mucho tiempo
// En el desarrollo real, este tipo de cálculo debería colocarse en un Isolate separado
var result = fibonacci(40);
var elapsed = DateTime.now().difference(startTime);
print('EJEMPLO Fibonacci(40) = $result');
print('Tiempo: ${elapsed.inMilliseconds}ms');
print('(Nota: durante el cálculo, el Isolate principal no puede procesar otras tareas)');
}
En aplicaciones Flutter, si se ejecuta un cálculo síncrono que consume mucho tiempo en el Isolate principal, la interfaz de usuario (UI) se congelará. La solución es colocar la tarea de cálculo en un Isolate separado y, una vez completado el cálculo, enviar el resultado mediante mensajes para actualizar la UI.
Mecanismo de paso de mensajes
Los Isolate se comunican entre sí mediante SendPort y ReceivePort.
Los mensajes deben ser serializables (tipos básicos, String, List, Map, etc.); no se pueden pasar funciones ni cierres (closures).
Comunicación bidireccional
Ejemplo
// Función de trabajo que se ejecuta en el nuevo Isolate
void workerIsolate(SendPort mainSendPort) {
// Crea su propio ReceivePort para recibir mensajes del Isolate principal
var workerReceivePort = ReceivePort();
// Primero envía el SendPort del worker al Isolate principal para establecer un canal bidireccional
mainSendPort.send(workerReceivePort.sendPort);
print('Worker Isolate: esperando tareas...');
// Escucha las tareas enviadas por el Isolate principal
workerReceivePort.listen((message) {
if (message is List<int>) {
// Recibe la tarea: calcular el cuadrado de cada número de la lista
print('Worker Isolate: datos recibidos $message');
var result = message.map((n) => n * n).toList();
// Envía el resultado de vuelta mediante mainSendPort
mainSendPort.send(result);
} else if (message == 'exit') {
print('Worker Isolate: señal de salida recibida, cerrando');
workerReceivePort.close();
mainSendPort.send('goodbye');
}
});
}
Future<void> main() async {
print('Isolate principal: inicio');
// Crea el puerto de recepción principal
var mainReceivePort = ReceivePort();
// Inicia el Worker Isolate
await Isolate.spawn(workerIsolate, mainReceivePort.sendPort);
// Espera a que el Worker envíe su SendPort (para establecer comunicación bidireccional)
SendPort? workerSendPort;
await for (var msg in mainReceivePort) {
if (msg is SendPort) {
workerSendPort = msg;
print('Isolate principal: comunicación bidireccional establecida con el Worker');
break;
}
}
// Envía la tarea a través del SendPort del Worker
workerSendPort!.send([1, 2, 3, 4, 5]);
workerSendPort.send([10, 20, 30]);
// Recibe el resultado del cálculo del Worker
int responseCount = 0;
await for (var msg in mainReceivePort) {
if (msg is List<int>) {
print('Isolate principal: resultado recibido $msg');
responseCount++;
if (responseCount == 2) break;
}
}
// Envía la señal de salida
workerSendPort.send('exit');
await for (var msg in mainReceivePort) {
if (msg == 'goodbye') {
print('Isolate principal: el Worker ha salido');
break;
}
}
mainReceivePort.close();
print('EJEMPLO: fin de la demostración de comunicación bidireccional');
}
主 Isolate: 启动 主 Isolate: 已建立与 Worker 的双向通信 Worker Isolate: 等待任务... Worker Isolate: 收到数据 [1, 2, 3, 4, 5] 主 Isolate: 收到结果 [1, 4, 9, 16, 25] Worker Isolate: 收到数据 [10, 20, 30] 主 Isolate: 收到结果 [100, 400, 900] Worker Isolate: 收到退出信号,关闭 主 Isolate: Worker 已退出 EJEMPLO 双向通信演示结束
Simplificación con Isolate.run (Dart 3.0+)
Dart 3.0 introdujo Isolate.run(), lo que simplifica enormemente el uso para tareas de cálculo únicas.
Ejemplo
// Una función de cálculo que consume mucho tiempo
int complexCalculation(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
result += i * i;
}
return result;
}
Future<void> main() async {
print('Comenzando el cálculo...');
// Isolate.run: una sola línea lo resuelve todo; crea el Isolate automáticamente, ejecuta y devuelve el resultado
var result = await Isolate.run(() => complexCalculation(10000000));
print('EJEMPLO: resultado del cálculo: $result');
print('Cálculo completado');
// Comparación: si no se usa Isolate
print('(Si se calcula directamente en el Isolate principal, se bloquearán otras operaciones)');
}
开始计算... EJEMPLO 计算结果: 333333283333335000000 计算完成 (如果在主 Isolate 中直接计算,会阻塞其他操作)
Isolate.run() es adecuado para escenarios de "ejecutar un cálculo y devolver el resultado". Si necesitas una comunicación continua y bidireccional (como un servicio en segundo plano de larga duración), debes usar Isolate.spawn() + SendPort/ReceivePort.
Limitaciones del paso de mensajes en Isolate
| Se puede pasar | No se puede pasar |
|---|---|
| null、bool、int、double、String | Funciones, cierres (closures) |
| List, Map, Set (los elementos también se pueden pasar) | Stream、Future |
| SendPort (para establecer la cadena de comunicación) | La mayoría de los objetos de tipos no básicos |
| Capability (token de permisos) | Descriptores de archivo, sockets de red |
| TransferableTypedData (para transferir grandes bloques de datos de manera eficiente) | Clases personalizadas (a menos que sean serializables) |
Comparación de modelos de concurrencia
| Característica | Dart Isolate | Multihilo tradicional (como Java) |
|---|---|---|
| Modelo de memoria | Memoria independiente, sin compartir | Memoria compartida |
| Método de comunicación | Paso de mensajes (SendPort) | Variables compartidas + bloqueos (locks) |
| Competencia de datos | No existe (sin compartición) | Requiere protección con mecanismos de bloqueo |
| Riesgo de bloqueo mutuo (deadlock) | Casi inexistente | Existe |
| Costo de creación | Relativamente grande | Relativamente pequeño |
| Escenarios aplicables | Computación paralela intensiva en CPU | Concurrencia general |
Otras extensionesEl modelo de Isolate de Dart, aunque evita la carrera de datos, tiene la desventaja de un mayor costo de creación y comunicación. No crees miles de Isolates; normalmente unos pocos son suficientes. Para tareas intensivas en E/S, usar async/await es la mejor opción.