Concurrencia en Dart
Usa isolates para permitir la ejecución de código en paralelo en múltiples núcleos de procesador.
Esta página contiene una descripción conceptual de cómo funciona la programación concurrente en Dart. Explica el bucle de eventos (event loop), las características asíncronas del lenguaje y los isolates desde un nivel general. Para obtener ejemplos de código más prácticos sobre el uso de la concurrencia en Dart, lee la página de Programación asíncrona y la página de Isolates.
La programación concurrente en Dart se refiere tanto a las APIs asíncronas, como Future
y Stream, como a los isolates, que te permiten mover procesos a
núcleos separados.
Todo el código de Dart se ejecuta en isolates, comenzando en el isolate principal predeterminado, y expandiéndose opcionalmente a cualquier otro isolate subsiguiente que crees explícitamente. Cuando creas (spawn) un nuevo isolate, este tiene su propia memoria aislada y su propio bucle de eventos (event loop). El bucle de eventos es lo que hace posible la programación asíncrona y concurrente en Dart.
Bucle de eventos (event loop)
#El modelo de ejecución (runtime) de Dart se basa en un bucle de eventos (event loop). El bucle de eventos es responsable de ejecutar el código de tu programa, recopilar y procesar eventos, entre otras cosas.
A medida que se ejecuta tu aplicación, todos los eventos se añaden a una cola, llamada cola de eventos (event queue). Los eventos pueden ser cualquier cosa, desde solicitudes para volver a dibujar la interfaz de usuario, hasta pulsaciones de botones y teclas por parte del usuario, o E/S (I/O) desde el disco. Debido a que tu aplicación no puede predecir en qué orden ocurrirán los eventos, el bucle de eventos procesa los eventos en el orden en que se encolan, uno a la vez.
La forma en que funciona el bucle de eventos se asemeja a este código:
while (eventQueue.waitForEvent()) {
eventQueue.processNextEvent();
}
Este bucle de eventos de ejemplo es síncrono y se ejecuta en un solo hilo (thread). Sin embargo, la mayoría de las aplicaciones de Dart necesitan hacer más de una cosa a la vez. Por ejemplo, una aplicación cliente podría necesitar ejecutar una solicitud HTTP, al mismo tiempo que escucha cuando un usuario pulsa un botón. Para manejar esto, Dart ofrece muchas APIs asíncronas, como Futures, Streams y async-await. Estas APIs están construidas alrededor de este bucle de eventos.
Por ejemplo, considera realizar una solicitud de red:
http.get('https://example.com').then((response) {
if (response.statusCode == 200) {
print('Success!');
}
}
Cuando este código llega al bucle de eventos, llama inmediatamente a la
primera cláusula, http.get, y devuelve un Future.
También le indica al bucle de eventos que mantenga el callback en la cláusula then()
hasta que
se resuelva la solicitud HTTP. Cuando eso suceda, debe
ejecutar ese callback, pasando el resultado de la solicitud como argumento.
Este mismo modelo es, por lo general, cómo el bucle de eventos maneja todos los demás
eventos asíncronos en Dart, como los objetos Stream.
Programación asincrónica
#Esta sección resume los diferentes tipos y sintaxis de la programación asíncrona en Dart.
Si ya estás familiarizado con Future, Stream y async-await,
entonces puedes pasar directamente a la sección de isolates.
Futures
#Un Future representa el resultado de una operación asíncrona que eventualmente
se completará con un valor o un error.
En este código de muestra, el tipo de retorno de Future<String> representa una
promesa de proporcionar eventualmente un valor String (o un error).
Future<String> _readFileAsync(String filename) {
final file = File(filename);
// .readAsString() returns a Future.
// .then() registers a callback to be executed when `readAsString` resolves.
return file.readAsString().then((contents) {
return contents.trim();
});
}
La sintaxis async-await
#Las palabras clave async y await proporcionan una forma declarativa de definir
funciones asíncronas y usar sus resultados.
Aquí tienes un ejemplo de código síncrono que bloquea mientras espera la E/S (I/O) de un archivo:
const String filename = 'with_keys.json';
void main() {
// Read some data.
final fileData = _readFileSync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
String _readFileSync() {
final file = File(filename);
final contents = file.readAsStringSync();
return contents.trim();
}
Aquí tienes un código similar, pero con cambios (resaltados) para hacerlo asíncrono:
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final fileData = await _readFileAsync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}
La función main() utiliza la palabra clave await delante de _readFileAsync()
para permitir que otro código de Dart (como los manejadores de eventos) use la CPU mientras se ejecuta
el código nativo (E/S de archivos). El uso de await también tiene el efecto de convertir
el Future<String> devuelto por _readFileAsync() en un
String. Como
resultado, la variable contents tiene el tipo implícito String.
Como muestra la siguiente figura, el código de Dart se pausa mientras readAsString()
ejecuta código que no es de Dart, ya sea en el runtime de Dart o en el sistema operativo.
Una vez que readAsString() devuelve un valor, se reanuda la ejecución del código de Dart.
Streams
#Dart también admite código asíncrono en forma de streams. Los streams
proporcionan valores en el futuro y repetidamente a lo largo del tiempo. Una promesa de proporcionar una
serie de valores int a lo largo del tiempo tiene el tipo Stream<int>.
En el siguiente ejemplo, el stream creado con Stream.periodic
emite repetidamente un nuevo valor int cada segundo.
Stream<int> stream = Stream.periodic(const Duration(seconds: 1), (i) => i * i);
await-for y yield
#Await-for es un tipo de bucle for que ejecuta cada iteración posterior del
bucle a medida que se proporcionan nuevos valores. En otras palabras, se utiliza para "iterar sobre"
streams. En este ejemplo, se emitirá un nuevo valor desde la función
sumStream a medida que se emitan nuevos valores desde el stream que se proporciona como un
argumento. La palabra clave yield se utiliza en lugar de return
en funciones que
devuelven streams de valores.
Stream<int> sumStream(Stream<int> stream) async* {
var sum = 0;
await for (final value in stream) {
yield sum += value;
}
}
Si deseas obtener más información sobre el uso de async, await, Streams y
Futures, consulta el tutorial de programación asíncrona.
Isolates
#Dart admite la concurrencia a través de isolates, además de las APIs asíncronas. La mayoría de los dispositivos modernos tienen CPUs multinúcleo. Para aprovechar múltiples núcleos, los desarrolladores a veces utilizan hilos (threads) de memoria compartida que se ejecutan simultáneamente. Sin embargo, la concurrencia con estado compartido es propensa a errores y puede llevar a código complicado.
En lugar de hilos (threads), todo el código de Dart se ejecuta dentro de isolates. Al usar isolates, tu código de Dart puede realizar múltiples tareas independientes a la vez, utilizando núcleos de procesador adicionales si están disponibles. Los isolates son como hilos o procesos, pero cada isolate tiene su propia memoria y un solo hilo que ejecuta un bucle de eventos (event loop).
Cada isolate tiene sus propios campos globales, lo que garantiza que ninguna parte del estado de un isolate sea accesible desde ningún otro isolate. Los isolates solo pueden comunicarse entre sí mediante el paso de mensajes. Al no haber estado compartido entre isolates, complejidades de concurrencia como mutexes o locks y condiciones de carrera de datos (data races) no ocurrirán en Dart. Dicho esto, los isolates no previenen por completo las condiciones de carrera en general. Para obtener más información sobre este modelo de concurrencia, lee sobre el modelo de Actores (Actor model).
El isolate principal
#En la mayoría de los casos, no necesitas pensar en los isolates en absoluto. Los programas de Dart se ejecutan en el isolate principal de forma predeterminada. Es el hilo (thread) donde un programa comienza a ejecutarse y ejecutarse, como se muestra en la siguiente figura:
Incluso los programas con un solo isolate pueden ejecutarse sin problemas. Antes de continuar con la siguiente línea de código, estas aplicaciones usan async-await para esperar a que se completen las operaciones asíncronas. Una aplicación con buen comportamiento se inicia rápidamente, llegando al bucle de eventos lo antes posible. Luego, la aplicación responde a cada evento encolado con prontitud, utilizando operaciones asíncronas según sea necesario.
El ciclo de vida de un isolate
#Como muestra la siguiente figura,
cada isolate comienza ejecutando algo de código de Dart,
como la función main().
Este código de Dart podría registrar algunos escuchas de eventos (event listeners)—por
ejemplo, para responder a la entrada del usuario o a la E/S de archivos.
Cuando la función inicial del isolate retorna,
el isolate permanece si necesita manejar eventos.
Después de manejar los eventos, el isolate sale.
Manejo de eventos
#En una aplicación cliente, la cola de eventos del isolate principal puede contener solicitudes de repintado y notificaciones de pulsaciones (taps) y otros eventos de la interfaz de usuario. Por ejemplo, la siguiente figura muestra un evento de repintado, seguido de un evento de pulsación, seguido de dos eventos de repintado. El bucle de eventos toma eventos de la cola en orden de llegada (primero en entrar, primero en salir).
El manejo de eventos ocurre en el isolate principal después de que main() sale. En la
siguiente figura, después de que main() sale, el isolate principal maneja el primer
evento de repintado. Después de eso, el isolate principal maneja el evento de pulsación, seguido de un
evento de repintado.
Si una operación síncrona toma demasiado tiempo de procesamiento, la aplicación puede volverse lenta o no responder. En la siguiente figura, el código del controlador de pulsaciones tarda demasiado tiempo, por lo que los eventos subsiguientes se manejan demasiado tarde. La aplicación podría parecer congelada y cualquier animación que realice podría verse entrecortada.
En las aplicaciones cliente, el resultado de una operación síncrona demasiado larga suele ser una animación de la interfaz de usuario entrecortada (no fluida). Peor aún, la interfaz de usuario podría dejar de responder por completo.
Trabajadores en segundo plano (background workers)
#Si la interfaz de usuario de tu aplicación deja de responder debido a un cálculo que consume mucho tiempo (por ejemplo, analizar un archivo JSON grande), considera delegar ese cálculo a un isolate trabajador, a menudo llamado trabajador en segundo plano (background worker). Un caso común, mostrado en la siguiente figura, es crear (spawn) un isolate trabajador simple que realiza un cálculo y luego sale. El isolate trabajador devuelve su resultado en un mensaje cuando sale.
Un isolate trabajador puede realizar E/S (por ejemplo, leer y escribir archivos), configurar temporizadores y más. Tiene su propia memoria y no comparte ningún estado con el isolate principal. El isolate trabajador puede bloquearse sin afectar a otros isolates.
Usar isolates
#Hay dos formas de trabajar con isolates en Dart, dependiendo del caso de uso:
- Usa
Isolate.run()para realizar un único cálculo en un hilo (thread) separado. - Usa
Isolate.spawn()para crear un isolate que manejará múltiples mensajes a lo largo del tiempo, o un trabajador en segundo plano. Para obtener más información sobre cómo trabajar con isolates de larga duración, lee la página de Isolates.
En la mayoría de los casos, Isolate.run es la API recomendada
para ejecutar procesos en segundo plano.
Isolate.run()
#
El método estático Isolate.run() requiere un argumento: un callback que se
ejecutará en el isolate recién creado.
int slowFib(int n) => n <= 1 ? 1 : slowFib(n - 1) + slowFib(n - 2);
// Compute without blocking current isolate.
void fib40() async {
var result = await Isolate.run(() => slowFib(40));
print('Fib(40) = $result');
}
Rendimiento y grupos de isolates
#Cuando un isolate llama a Isolate.spawn(), ambos isolates tienen el mismo
código ejecutable y están en el mismo grupo de isolates (isolate group). Los grupos de isolates permiten
optimizaciones de rendimiento como compartir código; un nuevo isolate ejecuta inmediatamente
el código propiedad del grupo de isolates. Además, Isolate.exit() funciona solo cuando los
isolates están en el mismo grupo de isolates.
En algunos casos especiales, es posible que debas usar Isolate.spawnUri(), que
configura el nuevo isolate con una copia del código que está en la URI especificada.
Sin embargo, spawnUri() es mucho más lento que spawn(), y el nuevo isolate no está
en el grupo de isolates de quien lo creó (spawner). Otra consecuencia de rendimiento es que el paso de
mensajes es más lento cuando los isolates están en grupos diferentes.
Limitaciones de los isolates
#Los isolates no son hilos (threads)
#Si vienes a Dart desde un lenguaje con multihilo (multithreading), sería razonable esperar que los isolates se comporten como hilos (threads), pero ese no es el caso. Cada isolate tiene su propio estado, lo que garantiza que ninguna parte del estado de un isolate sea accesible desde ningún otro isolate. Por lo tanto, los isolates están limitados por su acceso a su propia memoria.
Por ejemplo, si tienes una aplicación con una variable global mutable, esa variable será una variable separada en tu isolate secundario. Si mutas esa variable en el isolate secundario, permanecerá intacta en el isolate principal. Así es como se supone que deben funcionar los isolates, y es importante tenerlo en cuenta cuando consideres usar isolates.
Tipos de mensajes
#Los mensajes enviados a través de SendPort
pueden ser casi cualquier tipo de objeto Dart, pero hay algunas excepciones:
- Objetos con recursos nativos, como
Socket. ReceivePortDynamicLibraryFinalizableFinalizerNativeFinalizerPointerUserTag- Instancias de clases que están marcadas con
@pragma('vm:isolate-unsendable')
Aparte de esas excepciones, se puede enviar cualquier objeto.
Consulta la documentación de SendPort.send
para obtener más información.
Ten en cuenta que Isolate.spawn() e Isolate.exit() se abstraen sobre objetos SendPort
, por lo que están sujetos a las mismas limitaciones.
Comunicación síncrona bloqueante entre isolates
#Existe un límite para la cantidad de isolates que pueden ejecutarse en paralelo. Este límite no afecta a la comunicación asíncrona estándar entre isolates a través de mensajes en Dart. Puedes tener cientos de isolates ejecutándose simultáneamente y progresando. Los isolates se programan en la CPU de forma round-robin y se ceden el paso entre sí a menudo.
Los isolates solo pueden comunicarse síncronamente fuera de Dart puro, utilizando código C a través de FFI para hacerlo. Intentar la comunicación síncrona entre isolates mediante bloqueo síncrono en llamadas FFI podría resultar en un punto muerto (deadlock) si el número de isolates supera el límite, a menos que se tenga un cuidado especial. El límite no está predefinido en un número particular, se calcula en función del tamaño del montón (heap) de la Dart VM disponible para la aplicación Dart.
Para evitar esta situación, el código C que realiza el bloqueo síncrono
debe salir del isolate actual antes de realizar la operación de bloqueo
y volver a ingresar a él antes de retornar a Dart desde la llamada FFI.
Lee sobre Dart_EnterIsolate
y Dart_ExitIsolate
para obtener más información.
Concurrencia en la web
#Todas las aplicaciones Dart pueden usar async-await, Future y Stream
para cálculos intercalados no bloqueantes. La plataforma web de Dart, sin embargo,
no admite isolates. Las aplicaciones web de Dart pueden usar web workers
para ejecutar scripts
en hilos de fondo similares a los isolates. Sin embargo, la funcionalidad y las
capacidades de los web workers difieren un poco de las de los isolates.
Por ejemplo, cuando los web workers envían datos entre hilos, copian los datos de un lado a otro. Sin embargo, la copia de datos puede ser muy lenta, especialmente para mensajes grandes. Los isolates hacen lo mismo, pero también proporcionan APIs que pueden transferir de manera más eficiente la memoria que contiene el mensaje en su lugar.
La creación de web workers e isolates también difiere. Solo puedes crear web workers
declarando un punto de entrada del programa separado y compilándolo por separado. Iniciar
un web worker es similar a usar Isolate.spawnUri para iniciar un isolate. También puedes
iniciar un isolate con Isolate.spawn, lo que requiere menos recursos
porque
reutiliza parte del mismo código y datos
que el isolate creador. Los web workers no tienen una API equivalente.
Recursos adicionales
#- Si estás usando muchos isolates, considera
el
IsolateNameServeren Flutter, opackage:isolate_name_serverque proporciona una funcionalidad similar para aplicaciones Dart que no son de Flutter. - Lee más sobre el modelo de Actores (Actor model), en el cual se basan los isolates de Dart.
-
Additional documentation on
IsolateAPIs:
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 2026-05-15. Ver fuente oreportar un problema.