Entendiendo los Isolates y la falta de memoria compartida

En Dart, cuando necesitas ejecutar código de forma concurrente, no utilizas hilos con memoria compartida, sino Isolates. Un Isolate es una unidad de ejecución independiente que posee su propio heap de memoria (su propio espacio de gestión de objetos) y su propio event loop. A diferencia de otros lenguajes donde varios hilos pueden intentar modificar la misma variable simultáneamente —provocando condiciones de carrera (race conditions)—, en Dart esto es imposible porque los isolates no comparten memoria; solo pueden comunicarse mediante el paso de mensajes. Esta arquitectura elimina la necesidad de usar locks o mutexes para evitar la corrupción de datos, simplificando drásticamente el razonamiento sobre el estado. Debes recurrir a los isolates únicamente cuando tengas tareas pesadas de CPU (como procesamiento de imágenes o cálculos matemáticos complejos) que puedan bloquear el event loop del isolate principal. Si intentas acceder o modificar una variable global desde un isolate hijo, no estarás afectando al isolate principal, ya que cada uno opera en su propia “burbuja” de memoria.

import 'dart:isolate';

// El código de la función debe ser una función de nivel superior 
// o un método estático para que el isolate pueda cargarla.
void calcularSumaPesada(SendPort sendPort) {
  int suma = 0;
  // Simulamos una carga pesada de CPU
  for (int i = 0; i < 100_000_000; i++) {
    suma += i;
  }

  // Enviamos el resultado de vuelta al isolate principal mediante un mensaje.
  // Aquí no estamos compartiendo la variable 'suma', estamos enviando su valor.
  sendPort.send(suma);
}

void main() async {
  print('Main: Iniciando proceso pesado...');

  // Creamos un ReceivePort que actuará como nuestro "buzón" para recibir mensajes.
  final receivePort = ReceivePort();

  // Spawn crea un nuevo isolate y le pasa la dirección de nuestro buzón.
  // La función 'calcularSumaPesada' se ejecuta en su propio heap.
  await Isolate.spawn(calcularSumaPesada, receivePort.sendPort);

  print('Main: El isolate principal sigue libre para otras tareas.');

  // El main se queda escuchando el puerto de recepción.
  // .first es una forma limpia de esperar el primer mensaje disponible.
  final resultado = await receivePort.receive!;

  print('Main: La suma calculada por el isolate es: $resultado');
}

Fíjate en cómo main y calcularSumaPesada operan de forma totalmente aislada. Al ejecutar Isolate.spawn, la VM de Dart no le da al nuevo isolate acceso a las variables locales de main. La única conexión entre ambos es el SendPort que le pasamos. En el ReceivePort, el método receive! suspende la ejecución del main (de forma no bloqueante, gracias al await) hasta que el mensaje llega. Es vital entender que cuando hacemos sendPort.send(suma), el valor se copia o se transfiere; no es una referencia al espacio de memoria del hijo. Si el isolate hijo modificara una variable global después de enviar el resultado, esa modificación sería invisible para el main.

Además, aunque los isolates son más pesados que un hilo ligero (thread) porque cada uno tiene su propio stack y heap, son mucho más ligeros que un proceso del sistema operativo. Es un modelo similar al modelo de actores (como en Erlang), donde la comunicación es la clave. Cuando el isolate hijo termina, su memoria se libera de forma independiente, evitando que fugas de memoria en un hilo afecten al resto del programa.

El modelo de Dart permite concurrencia (gestionar múltiples tareas que progresan en el tiempo) y, si la VM utiliza múltiples núcleos de la CPU, permite el paralelismo real (ejecutar tareas exactamente al mismo tiempo en hardware distinto).

El error frecuente

Un error clásico es asumir que las variables static o globales se comparten entre isolates.

int contadorGlobal = 0; // Variable en el isolate principal

void worker(SendPort port) {
  contadorGlobal = 100; // ¿Esto cambiará el valor en el main?
  port.send(null);
}

void main() async {
  final port = ReceivePort();
  await Isolate.spawn(worker, port.sendPort);
  await port.receive;
  
  print(contadorGlobal); // Imprimirá 0, no 100.
}

Aunque veas contadorGlobal en ambos lugares, lo que ocurre es que el isolate hijo tiene su propia copia de la variable en su propio heap. Modificarla en el hijo es como copiar un papel, escribir en él y luego tirar el papel a la basura; el papel original en el main permanece intacto.

71

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio