Rust
Tutorial Rust: Fundamentos asíncronos y futures
Aprende los conceptos clave de asincronía, futures y ejecutores en Rust para desarrollar aplicaciones concurrentes eficientes y escalables.
Aprende Rust y certifícateQué es la asincronía
La asincronía es un paradigma de programación que permite a nuestro código realizar múltiples operaciones sin bloquear la ejecución principal del programa. A diferencia de la programación secuencial tradicional, donde cada instrucción debe completarse antes de pasar a la siguiente, la programación asíncrona nos permite iniciar una operación y continuar con otras tareas mientras esperamos que la primera finalice.
En el contexto de sistemas informáticos, muchas operaciones dependen de recursos externos que pueden tomar tiempo considerable en responder:
- Lectura y escritura de archivos en disco
- Solicitudes a servidores remotos a través de la red
- Consultas a bases de datos
- Espera de entrada del usuario
Estas operaciones se caracterizan por ser I/O bound (limitadas por entrada/salida), lo que significa que el tiempo de espera no está relacionado con la capacidad de procesamiento de la CPU, sino con factores externos.
Diferencia entre concurrencia con hilos y asincronía
Aunque ya conocemos la concurrencia basada en hilos, la asincronía ofrece un enfoque fundamentalmente distinto:
Concurrencia con hilos: Cada hilo representa una secuencia de ejecución independiente que el sistema operativo programa en los núcleos de CPU disponibles. Los hilos son recursos del sistema relativamente costosos, con un tamaño de pila fijo y sobrecarga de cambio de contexto.
Concurrencia asíncrona: Utiliza un número reducido de hilos (a menudo solo uno) para gestionar muchas tareas concurrentes. Las tareas pueden suspenderse cuando esperan recursos externos y reactivarse cuando los datos están disponibles, sin bloquear el hilo subyacente.
Veamos un ejemplo conceptual de la diferencia:
// Enfoque con hilos (bloqueante)
fn main() {
// Cada conexión requiere un hilo completo
for _ in 0..10_000 {
std::thread::spawn(|| {
// Este hilo queda bloqueado durante la espera
let datos = leer_de_red_bloqueante();
procesar(datos);
});
}
// Problema: 10,000 hilos consumen muchos recursos del sistema
}
// Enfoque asíncrono (no bloqueante)
fn main() {
// Un solo hilo puede manejar miles de tareas
let runtime = crear_ejecutor();
for _ in 0..10_000 {
runtime.spawn(async {
// Esta tarea se suspende durante la espera, liberando el hilo
let datos = leer_de_red_asincrono().await;
procesar(datos);
});
}
// Ventaja: Mucho más eficiente en recursos
}
Modelo de ejecución asíncrona
La asincronía en Rust se basa en un modelo de suspensión y reanudación de tareas. Cuando una operación asíncrona necesita esperar por un recurso externo:
- La tarea se suspende y guarda su estado actual
- El hilo queda libre para trabajar en otras tareas
- Cuando los datos están disponibles, la tarea se reanuda desde donde se quedó
Este modelo es particularmente eficiente para aplicaciones que manejan muchas operaciones de I/O simultáneas, como servidores web, aplicaciones de red o sistemas que procesan muchas solicitudes concurrentes.
Ventajas de la programación asíncrona
Escalabilidad mejorada: Un solo hilo puede manejar miles de tareas concurrentes, lo que permite crear sistemas que escalan mejor con recursos limitados.
Menor consumo de memoria: Al no necesitar un hilo completo por tarea, se reduce significativamente el uso de memoria del sistema.
Menos cambios de contexto: Los cambios entre tareas asíncronas son más ligeros que los cambios de contexto entre hilos del sistema operativo.
Mayor rendimiento en escenarios I/O bound: Para aplicaciones que esperan constantemente por operaciones de entrada/salida, la asincronía puede mejorar drásticamente el rendimiento.
Cuándo usar asincronía
La programación asíncrona no es una solución universal. Es especialmente adecuada para:
- Aplicaciones con muchas operaciones de I/O concurrentes
- Servidores que manejan miles de conexiones simultáneas
- Sistemas que requieren alta eficiencia en recursos
- Aplicaciones que necesitan mantener responsividad mientras esperan recursos externos
Sin embargo, para tareas CPU bound (limitadas por procesamiento) que requieren cálculos intensivos, la concurrencia basada en hilos o paralelismo puede ser más apropiada, ya que aprovecha múltiples núcleos de CPU.
Desafíos de la programación asíncrona
Aunque la asincronía ofrece ventajas significativas, también presenta algunos desafíos:
Complejidad conceptual: El flujo de ejecución no lineal puede ser más difícil de seguir y razonar.
Propagación de asincronía: Una vez que introduces asincronía en parte de tu código, tiende a "propagarse" a través de la base de código.
Depuración más compleja: Seguir el flujo de ejecución y diagnosticar problemas puede ser más difícil en código asíncrono.
Sobrecarga de abstracción: Existe cierta sobrecarga asociada con la maquinaria que permite la suspensión y reanudación de tareas.
Modelo mental para la asincronía en Rust
En Rust, podemos visualizar la asincronía como un sistema donde:
- Las tareas asíncronas representan unidades de trabajo que pueden suspenderse
- Un ejecutor (o runtime) es responsable de avanzar estas tareas cuando pueden progresar
- Las tareas se suspenden en puntos de espera cuando necesitan recursos externos
- El ejecutor puede multiplexar muchas tareas en un número reducido de hilos
Este modelo es fundamentalmente diferente del enfoque basado en callbacks común en otros lenguajes, y ofrece un código más legible y mantenible a largo plazo.
// Ejemplo conceptual (simplificado) de una tarea asíncrona
fn tarea_asincrona() -> impl Future<Output = String> {
async {
// Punto 1: Código que se ejecuta inmediatamente
let id = generar_id();
// Punto 2: Aquí la tarea se suspenderá, liberando el hilo
let respuesta = obtener_datos_de_red(id).await;
// Punto 3: Cuando los datos lleguen, la ejecución continúa aquí
procesar_respuesta(respuesta)
}
}
En este ejemplo conceptual, la tarea puede suspenderse mientras espera datos de la red, permitiendo que el hilo trabaje en otras tareas. Cuando los datos están disponibles, la tarea se reanuda exactamente desde donde se quedó.
La asincronía en Rust proporciona una forma elegante y eficiente de manejar operaciones concurrentes, especialmente aquellas que involucran esperas por recursos externos, sin la sobrecarga asociada con la creación y gestión de múltiples hilos.
Futures en Rust
Los futures son el componente fundamental del sistema de asincronía en Rust. Un future representa una operación asíncrona que puede no haber completado su ejecución todavía. Podemos pensar en ellos como una promesa de que eventualmente tendremos un valor resultante.
A diferencia de otros lenguajes que implementan promesas o futuros basados en callbacks, Rust utiliza un enfoque basado en polling (sondeo), lo que permite un control más preciso sobre la ejecución y evita algunos problemas comunes como la inversión de control.
Trait Future
En Rust, los futures se definen mediante el trait Future
de la biblioteca estándar:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
Este trait contiene:
Output
: El tipo de valor que el future eventualmente producirápoll
: El método que intenta avanzar el future hacia su finalización
El tipo de retorno Poll
es una enumeración con dos variantes:
enum Poll<T> {
Ready(T), // El future ha completado con un valor T
Pending, // El future aún no está listo y necesita ser sondeado más tarde
}
Modelo de ejecución basado en polling
El modelo de polling de Rust funciona de la siguiente manera:
- El ejecutor llama al método
poll()
del future - Si el future puede completarse, devuelve
Poll::Ready(valor)
- Si el future necesita esperar, devuelve
Poll::Pending
y registra un waker para notificar cuando pueda avanzar - Cuando el recurso externo está listo, el waker es invocado
- El ejecutor vuelve a llamar a
poll()
en el future
Este enfoque tiene varias ventajas:
- Evita la creación de callbacks anidados
- Permite un control más fino sobre la ejecución
- Facilita la composición de operaciones asíncronas
- Reduce la sobrecarga de memoria
Creación manual de un Future simple
Para entender mejor cómo funcionan los futures internamente, veamos un ejemplo simplificado de un future que se completa después de cierto tiempo:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
struct Delay {
when: Instant,
}
impl Future for Delay {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if Instant::now() >= self.when {
println!("¡Future completado!");
Poll::Ready(())
} else {
// Programamos ser despertados cuando sea el momento
let waker = cx.waker().clone();
let when = self.when;
// En un caso real, esto se haría con un temporizador adecuado
std::thread::spawn(move || {
let now = Instant::now();
if now < when {
std::thread::sleep(when - now);
}
waker.wake();
});
Poll::Pending
}
}
}
// Crear un future que se completará después de 2 segundos
fn delay(duration: Duration) -> Delay {
Delay {
when: Instant::now() + duration,
}
}
Este ejemplo muestra los componentes clave de un future:
- Implementación del trait
Future
- Lógica para determinar si está listo (
Poll::Ready
) o no (Poll::Pending
) - Mecanismo para notificar al ejecutor cuando el future puede avanzar (usando el
waker
)
Composición de Futures
Una de las grandes fortalezas de los futures es su capacidad de composición. Podemos combinar futures más simples para crear operaciones asíncronas más complejas:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// Un future que ejecuta dos futures en secuencia
struct ThenFuture<FutA, FutB, FnT> {
first: Option<FutA>, // El primer future a ejecutar
second: Option<FutB>, // El segundo future (puede ser None inicialmente)
transition: FnT, // Función para crear el segundo future
}
impl<FutA, FutB, FnT, A, B> Future for ThenFuture<FutA, FutB, FnT>
where
FutA: Future<Output = A>,
FutB: Future<Output = B>,
FnT: FnOnce(A) -> FutB,
{
type Output = B;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// Nota: Esta implementación es simplificada y no maneja correctamente Pin
// Si tenemos el primer future, intentamos avanzarlo
if let Some(first) = &mut self.first {
match Future::poll(Pin::new(first), cx) {
Poll::Ready(value) => {
// El primer future está listo, creamos el segundo
let second = (self.transition)(value);
self.first = None;
self.second = Some(second);
}
Poll::Pending => return Poll::Pending,
}
}
// Si tenemos el segundo future, intentamos avanzarlo
if let Some(second) = &mut self.second {
return Future::poll(Pin::new(second), cx);
}
unreachable!("No debería llegar aquí");
}
}
// Función auxiliar para crear un ThenFuture
fn then<FutA, FnT, FutB>(future: FutA, f: FnT) -> ThenFuture<FutA, FutB, FnT>
where
FutA: Future,
FnT: FnOnce(FutA::Output) -> FutB,
FutB: Future,
{
ThenFuture {
first: Some(future),
second: None,
transition: f,
}
}
Este ejemplo muestra cómo podríamos implementar una versión simplificada de la funcionalidad then
para encadenar futures. En la práctica, la biblioteca estándar y crates como futures
proporcionan implementaciones más robustas y optimizadas.
Futures y estado de la máquina
Internamente, los futures en Rust se implementan como máquinas de estado. Cada vez que un future se suspende (devuelve Poll::Pending
), su estado actual se guarda para poder reanudar la ejecución más tarde desde ese punto.
Cuando usamos la sintaxis async/await
(que veremos en lecciones posteriores), el compilador de Rust transforma nuestro código en una máquina de estado que implementa el trait Future
. Esto nos permite escribir código asíncrono de forma secuencial y legible, mientras que por debajo se genera código eficiente basado en futures.
Futures vs Threads: Cuándo usar cada uno
Los futures y los hilos tienen diferentes casos de uso:
Futures (asincronía): Ideal para operaciones de I/O concurrentes, como manejar miles de conexiones de red simultáneas con recursos limitados.
Threads (paralelismo): Mejor para tareas intensivas de CPU que pueden ejecutarse en paralelo, aprovechando múltiples núcleos.
Comparativa de rendimiento:
Escenario | Futures | Threads |
---|---|---|
Muchas operaciones I/O | Excelente (bajo consumo de recursos) | Deficiente (alto consumo de memoria) |
Tareas CPU-intensivas | Limitado (un solo hilo) | Excelente (paralelismo real) |
Sobrecarga por tarea | Muy baja (~100 bytes) | Alta (~1-8 MB por hilo) |
Cambio de contexto | Muy ligero | Más costoso |
Limitaciones de los Futures
Los futures en Rust tienen algunas limitaciones importantes:
No se ejecutan por sí solos: Un future no hace nada hasta que algo llama a su método
poll()
. Necesitan un ejecutor.No son thread-safe por defecto: Los futures básicos no implementan
Send
niSync
automáticamente, aunque muchas implementaciones concretas sí lo hacen.Complejidad de implementación manual: Implementar futures manualmente requiere entender conceptos avanzados como
Pin
yContext
.
// Ejemplo: Un future que no hace nada por sí solo
fn main() {
let future = async {
println!("¡Hola desde un future!");
};
// Este código NO imprimirá nada, porque nadie está ejecutando el future
println!("Fin del programa");
}
Futures en la biblioteca estándar vs crate futures
Rust proporciona el trait Future
en su biblioteca estándar, pero muchas utilidades adicionales están disponibles en el crate futures
:
// Usando futures de la biblioteca estándar
use std::future::Future;
// Usando el crate futures para funcionalidades adicionales
use futures::future::{self, FutureExt, TryFutureExt};
El crate futures
proporciona:
- Combinadores adicionales como
join
,select
,try_join
- Primitivas de sincronización asíncrona
- Adaptadores entre streams y futures
- Utilidades para trabajar con futures de forma más conveniente
Resumen
Los futures en Rust representan operaciones asíncronas que pueden completarse en el futuro. A diferencia de otros lenguajes, Rust utiliza un modelo basado en polling que ofrece mayor control y eficiencia. Los futures por sí solos no hacen nada; necesitan un ejecutor que los avance llamando a su método poll()
.
La verdadera potencia de los futures viene de su capacidad de composición, permitiendo construir operaciones asíncronas complejas a partir de otras más simples. Aunque implementar futures manualmente puede ser complejo, las herramientas de Rust como la sintaxis async/await
(que veremos más adelante) simplifican enormemente este proceso.
En la siguiente sección, exploraremos los ejecutores (runtimes) que son necesarios para hacer que los futures realmente funcionen.
Ejecutores (runtimes)
Los ejecutores (también conocidos como runtimes) son el componente esencial que permite que los futures en Rust cobren vida. Como vimos anteriormente, un future por sí solo es simplemente una descripción de una tarea asíncrona, pero no realiza ningún trabajo hasta que algo llama repetidamente a su método poll()
. Aquí es donde entran en juego los ejecutores.
Un ejecutor asíncrono es responsable de:
- Gestionar una colección de futures pendientes
- Llamar a
poll()
en los futures cuando pueden avanzar - Manejar las notificaciones de los wakers cuando un future está listo para progresar
- Distribuir eficientemente el trabajo entre los hilos disponibles
Funcionamiento básico de un ejecutor
El ciclo de vida típico de un future dentro de un ejecutor sigue este patrón:
- El future se registra con el ejecutor (generalmente mediante un método como
spawn
) - El ejecutor llama a
poll()
en el future - Si el future devuelve
Poll::Ready
, se completa y se elimina del ejecutor - Si devuelve
Poll::Pending
, el ejecutor lo guarda y registra su waker - Cuando el waker es activado, el ejecutor vuelve a poner el future en la cola para ser sondeado nuevamente
// Ejemplo conceptual simplificado de un ejecutor básico
struct MiniExecutor {
// Cola de futures listos para ser sondeados
ready_queue: Vec<Box<dyn Future<Output = ()> + Send>>,
}
impl MiniExecutor {
fn new() -> Self {
MiniExecutor {
ready_queue: Vec::new(),
}
}
// Añade un future al ejecutor
fn spawn<F>(&mut self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
self.ready_queue.push(Box::new(future));
}
// Ejecuta todos los futures hasta que se completen
fn run(&mut self) {
// Nota: Esta implementación es extremadamente simplificada
// y no maneja correctamente wakers ni Pin
while !self.ready_queue.is_empty() {
// Tomamos un future de la cola
let mut future = self.ready_queue.remove(0);
// Creamos un contexto con un waker que volvería a poner
// el future en la cola cuando sea activado
let waker = /* ... */;
let mut context = Context::from_waker(&waker);
// Sondeamos el future
match Pin::new(&mut future).poll(&mut context) {
Poll::Ready(()) => {
// El future ha completado, no hacemos nada más con él
}
Poll::Pending => {
// El future no está listo, lo guardamos para más tarde
// (en un ejecutor real, esto sería manejado por el waker)
self.ready_queue.push(future);
}
}
}
}
}
Este ejemplo es extremadamente simplificado y no es práctico para uso real, pero ilustra el concepto básico de cómo funciona un ejecutor.
Tipos de ejecutores
En el ecosistema de Rust existen varios tipos de ejecutores, cada uno con características y optimizaciones diferentes:
Ejecutores de un solo hilo: Procesan todos los futures en un único hilo. Son simples y tienen menos sobrecarga, pero no aprovechan múltiples núcleos.
Ejecutores multi-hilo: Distribuyen los futures entre varios hilos de trabajo, permitiendo paralelismo real. Son ideales para aplicaciones que necesitan alto rendimiento.
Ejecutores con I/O integrado: Incluyen integraciones optimizadas con sistemas de I/O asíncrono del sistema operativo (como epoll, kqueue, IOCP).
Ejecutores especializados: Diseñados para casos de uso específicos, como procesamiento de señales, aplicaciones embebidas o entornos con restricciones.
Principales ejecutores en el ecosistema Rust
Aunque Rust no incluye un ejecutor en su biblioteca estándar, existen varias implementaciones maduras y ampliamente utilizadas:
Tokio: El ejecutor más popular y completo, optimizado para aplicaciones de red. Incluye primitivas para I/O asíncrono, temporizadores, canales y más.
async-std: Una biblioteca que proporciona una API similar a la biblioteca estándar de Rust, pero con operaciones asíncronas.
smol: Un ejecutor minimalista y ligero que prioriza la simplicidad y el tamaño reducido.
futures: Incluye un ejecutor básico útil para casos simples o pruebas.
// Ejemplo usando Tokio
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("Iniciando tarea asíncrona...");
// Spawn crea una nueva tarea asíncrona gestionada por el ejecutor
tokio::spawn(async {
sleep(Duration::from_millis(100)).await;
println!("Tarea en segundo plano completada");
});
sleep(Duration::from_millis(200)).await;
println!("Tarea principal completada");
}
// Ejemplo usando async-std
use async_std::task;
use std::time::Duration;
#[async_std::main]
async fn main() {
println!("Iniciando tarea asíncrona...");
// Spawn crea una nueva tarea asíncrona
task::spawn(async {
task::sleep(Duration::from_millis(100)).await;
println!("Tarea en segundo plano completada");
});
task::sleep(Duration::from_millis(200)).await;
println!("Tarea principal completada");
}
Características importantes de los ejecutores
Al elegir o trabajar con un ejecutor, hay varias características importantes a considerar:
Modelo de concurrencia: Algunos ejecutores utilizan un modelo cooperativo donde las tareas deben ceder explícitamente el control, mientras que otros manejan esto automáticamente.
Estrategia de trabajo: Cómo se distribuyen las tareas entre los hilos de trabajo disponibles.
Manejo de bloqueo: Cómo maneja el ejecutor las operaciones que bloquean un hilo (como llamadas a sistema bloqueantes).
Integración con I/O: Qué tan eficientemente se integra con las APIs de I/O asíncrono del sistema operativo.
Soporte para cancelación: Si proporciona mecanismos para cancelar tareas en ejecución.
Ejecutor local vs global
Algunos ejecutores permiten dos modos de operación:
Ejecutor local: Existe solo dentro de un ámbito específico y se destruye cuando ese ámbito termina.
Ejecutor global: Disponible durante toda la vida de la aplicación, a menudo implementado como un singleton.
// Ejemplo de ejecutor local con Tokio
use tokio::runtime::Runtime;
use std::time::Duration;
fn main() {
// Creamos un ejecutor local
let rt = Runtime::new().unwrap();
// Ejecutamos un bloque asíncrono en este ejecutor
rt.block_on(async {
println!("Ejecutando en un runtime local");
tokio::time::sleep(Duration::from_millis(100)).await;
println!("Completado");
});
// El runtime se destruye cuando rt sale del ámbito
}
Trabajo bloqueante en ejecutores
Un desafío común al trabajar con ejecutores asíncronos es manejar operaciones que bloquean el hilo, como operaciones intensivas de CPU o llamadas a bibliotecas sincrónicas. Los ejecutores suelen proporcionar mecanismos específicos para esto:
// Manejo de trabajo bloqueante en Tokio
#[tokio::main]
async fn main() {
// Esta operación bloqueante se ejecutará en un pool de hilos separado
// para no bloquear el ejecutor principal
let resultado = tokio::task::spawn_blocking(|| {
// Operación intensiva de CPU o llamada bloqueante
std::thread::sleep(std::time::Duration::from_secs(1));
"Resultado de operación bloqueante"
}).await.unwrap();
println!("{}", resultado);
}
Implementación manual de un ejecutor simple
Para entender mejor cómo funciona un ejecutor, veamos una implementación muy simplificada que puede manejar futures básicos:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Wake};
use std::sync::{Arc, Mutex};
use std::collections::VecDeque;
use std::thread;
// Una implementación simple de Waker
struct SimpleWaker {
queue: Arc<Mutex<VecDeque<Arc<Task>>>>,
task: Arc<Task>,
}
impl Wake for SimpleWaker {
fn wake(self: Arc<Self>) {
// Cuando se activa el waker, ponemos la tarea de vuelta en la cola
self.queue.lock().unwrap().push_back(self.task.clone());
}
}
// Representa una tarea asíncrona
struct Task {
future: Mutex<Pin<Box<dyn Future<Output = ()> + Send>>>,
}
// Nuestro ejecutor simple
struct SimpleExecutor {
task_queue: Arc<Mutex<VecDeque<Arc<Task>>>>,
}
impl SimpleExecutor {
fn new() -> Self {
SimpleExecutor {
task_queue: Arc::new(Mutex::new(VecDeque::new())),
}
}
// Añade un future al ejecutor
fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
let task = Arc::new(Task {
future: Mutex::new(Box::pin(future)),
});
self.task_queue.lock().unwrap().push_back(task);
}
// Ejecuta todas las tareas hasta que se completen
fn run(&self) {
while let Some(task) = self.task_queue.lock().unwrap().pop_front() {
// Creamos un waker para esta tarea
let waker = Arc::new(SimpleWaker {
queue: self.task_queue.clone(),
task: task.clone(),
});
let waker = std::task::Waker::from(waker);
let mut context = Context::from_waker(&waker);
// Sondeamos el future
let mut future_lock = task.future.lock().unwrap();
if let Poll::Ready(()) = future_lock.as_mut().poll(&mut context) {
// La tarea ha completado, no hacemos nada más
}
// Si devuelve Pending, el waker se encargará de volver a ponerla en la cola
}
}
}
Este ejecutor es extremadamente básico y carece de muchas optimizaciones y características que tendría un ejecutor real, pero ilustra los conceptos fundamentales.
Consideraciones de rendimiento
Los ejecutores modernos implementan numerosas optimizaciones para maximizar el rendimiento:
Work stealing: Permite que los hilos inactivos "roben" trabajo de otros hilos ocupados, mejorando la utilización de recursos.
Colas de tareas optimizadas: Utilizan estructuras de datos sin bloqueo para minimizar la contención entre hilos.
Integración con I/O asíncrono del sistema: Se integran directamente con mecanismos como epoll (Linux), kqueue (BSD/macOS) o IOCP (Windows).
Estrategias de planificación inteligentes: Priorizan tareas basándose en diversos factores como tiempo de espera o dependencias.
Cuándo usar cada ejecutor
La elección del ejecutor depende de las necesidades específicas de tu aplicación:
Tokio: Ideal para aplicaciones de red de alto rendimiento, servidores web, o cualquier sistema que requiera escalabilidad y características completas.
async-std: Buena opción si prefieres una API familiar similar a la biblioteca estándar de Rust.
smol: Excelente para aplicaciones más pequeñas donde el tamaño del binario y la simplicidad son prioritarios.
Ejecutor personalizado: Puede ser necesario para entornos muy específicos como sistemas embebidos o cuando tienes requisitos únicos.
Interoperabilidad entre ejecutores
Un desafío importante en el ecosistema asíncrono de Rust es la interoperabilidad entre diferentes ejecutores. En general:
Los futures son independientes del ejecutor y pueden transferirse entre diferentes implementaciones.
Las primitivas específicas de cada ejecutor (como temporizadores o canales) generalmente no son transferibles.
Mezclar ejecutores en la misma aplicación puede llevar a problemas de rendimiento o comportamientos inesperados.
La mejor práctica es elegir un único ejecutor para toda tu aplicación siempre que sea posible.
Resumen
Los ejecutores son el motor que impulsa la asincronía en Rust, proporcionando el entorno necesario para que los futures se ejecuten eficientemente. Aunque Rust no incluye un ejecutor estándar, el ecosistema ofrece varias implementaciones maduras que satisfacen diferentes necesidades.
Al trabajar con asincronía en Rust, es importante entender que los futures y los ejecutores son conceptos separados pero complementarios: los futures describen "qué" trabajo asíncrono debe realizarse, mientras que los ejecutores determinan "cómo" y "cuándo" se realiza ese trabajo.
En las próximas lecciones, exploraremos cómo la sintaxis async/await
simplifica enormemente la escritura de código asíncrono, ocultando gran parte de la complejidad de los futures y ejecutores que hemos visto hasta ahora.
Otras lecciones de Rust
Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Rust
Introducción Y Entorno
Primer Programa
Introducción Y Entorno
Instalación Del Entorno
Introducción Y Entorno
Funciones
Sintaxis
Operadores
Sintaxis
Estructuras De Control Condicional
Sintaxis
Arrays Y Strings
Sintaxis
Manejo De Errores Panic
Sintaxis
Variables Y Tipos Básicos
Sintaxis
Estructuras De Control Iterativo
Sintaxis
Colecciones Estándar
Estructuras De Datos
Option Y Result
Estructuras De Datos
Pattern Matching
Estructuras De Datos
Estructuras (Structs)
Estructuras De Datos
Enumeraciones Enums
Estructuras De Datos
El Concepto De Ownership
Ownership
Lifetimes Básicos
Ownership
Slices Y Referencias Parciales
Ownership
References Y Borrowing
Ownership
Funciones Anónimas Closures
Abstracción
Traits De La Biblioteca Estándar
Abstracción
Traits
Abstracción
Generics
Abstracción
Channels Y Paso De Mensajes
Concurrencia
Memoria Compartida Segura
Concurrencia
Threads Y Sincronización Básica
Concurrencia
Introducción A Tokio
Asincronía
Fundamentos Asíncronos Y Futures
Asincronía
Async/await
Asincronía
Ejercicios de programación de Rust
Evalúa tus conocimientos de esta lección Fundamentos asíncronos y futures con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender el concepto de asincronía y su diferencia con la concurrencia basada en hilos.
- Entender el modelo de ejecución asíncrona en Rust mediante suspensión y reanudación de tareas.
- Conocer el trait Future, su método poll y el modelo de polling para avanzar tareas asíncronas.
- Aprender cómo funcionan los ejecutores (runtimes) para gestionar y ejecutar futures.
- Identificar las ventajas, desafíos y casos de uso adecuados para la programación asíncrona en Rust.