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:

NivelMecanismoEscenario 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
IsolateMemoria independiente + paso de mensajesCálculos intensivos en CPU
Future/StreamSintaxis de programación asíncronaLa 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

import 'dart:isolate';

// 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

// Ejecuta una tarea intensiva en CPU en el Isolate principal (bloqueará)
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

import 'dart:isolate';

// 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

import 'dart:isolate';

// 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 pasarNo se puede pasar
null、bool、int、double、StringFunciones, 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ísticaDart IsolateMultihilo tradicional (como Java)
Modelo de memoriaMemoria independiente, sin compartirMemoria compartida
Método de comunicaciónPaso de mensajes (SendPort)Variables compartidas + bloqueos (locks)
Competencia de datosNo existe (sin compartición)Requiere protección con mecanismos de bloqueo
Riesgo de bloqueo mutuo (deadlock)Casi inexistenteExiste
Costo de creaciónRelativamente grandeRelativamente pequeño
Escenarios aplicablesComputación paralela intensiva en CPUConcurrencia general

El 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.

Otras extensiones