Rust

Rust

Tutorial Rust: Async/Await

Aprende la sintaxis async/await en Rust para escribir código asíncrono claro y eficiente con manejo avanzado de errores y combinación de futures.

Aprende Rust y certifícate

Sintaxis async/await

La programación asíncrona en Rust se ha vuelto considerablemente más accesible gracias a la sintaxis async/await, que nos permite escribir código asíncrono de manera similar al código síncrono tradicional. Esta sintaxis nos ayuda a expresar operaciones que pueden suspenderse y reanudarse sin bloquear el hilo de ejecución principal.

Funciones asíncronas

En Rust, podemos declarar una función como asíncrona utilizando la palabra clave async:

async fn leer_archivo(ruta: &str) -> Result<String, std::io::Error> {
    // Código para leer un archivo de forma asíncrona
    std::fs::read_to_string(ruta) // Por simplicidad usamos la versión síncrona
}

Lo que ocurre internamente es que el compilador transforma esta función en una que devuelve un Future. Es importante entender que una función async no ejecuta su código inmediatamente cuando es llamada, sino que construye un Future que representa la computación.

El tipo de retorno real de la función anterior sería algo como:

fn leer_archivo(ruta: &str) -> impl Future<Output = Result<String, std::io::Error>> {
    // Implementación generada por el compilador
}

Sin embargo, la sintaxis async fn nos permite escribir código más limpio y expresivo sin tener que lidiar directamente con los tipos Future.

El operador await

Para obtener el valor de un Future, utilizamos el operador await. Este operador suspende la ejecución de la función actual hasta que el Future se complete:

async fn procesar_archivo(ruta: &str) -> Result<usize, std::io::Error> {
    let contenido = leer_archivo(ruta).await?;
    Ok(contenido.len())
}

Cuando se encuentra la expresión .await, la función asíncrona se suspende si el Future no está listo, permitiendo que el runtime ejecute otras tareas. Cuando el Future se completa, la ejecución continúa desde el punto donde se quedó.

Bloques async

Además de las funciones, también podemos crear bloques async para generar Futures en cualquier contexto:

fn obtener_future_longitud(ruta: &str) -> impl Future<Output = Result<usize, std::io::Error>> {
    async move {
        let contenido = leer_archivo(ruta).await?;
        Ok(contenido.len())
    }
}

Los bloques async son útiles cuando necesitamos crear Futures dentro de funciones síncronas o cuando queremos capturar variables del entorno usando move.

Async en métodos y funciones asociadas

La sintaxis async también funciona con métodos y funciones asociadas:

struct Archivo {
    ruta: String,
}

impl Archivo {
    async fn leer(&self) -> Result<String, std::io::Error> {
        std::fs::read_to_string(&self.ruta) // Simplificado
    }
    
    async fn crear(ruta: String) -> Result<Self, std::io::Error> {
        // Verificar que se puede crear el archivo
        std::fs::File::create(&ruta)?;
        Ok(Self { ruta })
    }
}

Limitaciones de async

Existen algunas limitaciones importantes que debemos conocer:

  • No se puede usar await fuera de un contexto async: El operador await solo puede utilizarse dentro de funciones o bloques async.
// Esto NO compila
fn función_síncrona() {
    let resultado = leer_archivo("config.txt").await; // Error
}

// Esto SÍ compila
fn función_síncrona_correcta() {
    let future = async {
        let resultado = leer_archivo("config.txt").await;
        // Hacer algo con resultado
    };
    
    // Ahora necesitamos ejecutar este future con un runtime
}
  • Los traits no pueden tener métodos async directamente: Para implementar comportamiento asíncrono en traits, se utilizan enfoques alternativos como retornar tipos que implementen Future.
trait Lector {
    // Esto NO es válido en Rust actual
    // async fn leer(&self) -> Result<Vec<u8>, std::io::Error>;
    
    // En su lugar, hacemos esto:
    fn leer(&self) -> impl Future<Output = Result<Vec<u8>, std::io::Error>> + Send + '_;
}

Async en closures

También podemos crear closures asíncronos utilizando la sintaxis async:

let lector = async |ruta: &str| -> Result<String, std::io::Error> {
    std::fs::read_to_string(ruta)
};

async fn usar_closure() {
    let resultado = lector("datos.txt").await;
    // Procesar resultado
}

Async recursivo

Las funciones asíncronas pueden ser recursivas, pero debemos tener cuidado con el tamaño del tipo de retorno:

async fn procesar_directorio(ruta: &str, profundidad: usize) -> Result<Vec<String>, std::io::Error> {
    if profundidad == 0 {
        return Ok(Vec::new());
    }
    
    let mut archivos = Vec::new();
    let entradas = std::fs::read_dir(ruta)?;
    
    for entrada in entradas {
        let entrada = entrada?;
        let ruta_entrada = entrada.path();
        
        if ruta_entrada.is_file() {
            archivos.push(ruta_entrada.to_string_lossy().to_string());
        } else if ruta_entrada.is_dir() {
            let subarchivos = procesar_directorio(
                ruta_entrada.to_str().unwrap(), 
                profundidad - 1
            ).await?;
            
            archivos.extend(subarchivos);
        }
    }
    
    Ok(archivos)
}

Patrones comunes con async/await

Veamos algunos patrones comunes al trabajar con async/await:

  • Ejecución secuencial: Las operaciones se ejecutan una tras otra.
async fn operación_secuencial() -> Result<(), Error> {
    let resultado1 = operación1().await?;
    let resultado2 = operación2(resultado1).await?;
    let resultado3 = operación3(resultado2).await?;
    
    Ok(())
}
  • Transformación de datos: Procesamos los datos obtenidos de una operación asíncrona.
async fn transformar_datos() -> Result<Procesado, Error> {
    let datos_crudos = obtener_datos().await?;
    let datos_procesados = procesar(datos_crudos);
    
    Ok(datos_procesados)
}
  • Inicialización asíncrona: Configuramos recursos de manera asíncrona.
struct Servicio {
    config: Configuración,
    conexión: Conexión,
}

impl Servicio {
    async fn new() -> Result<Self, Error> {
        let config = cargar_configuración().await?;
        let conexión = establecer_conexión(&config).await?;
        
        Ok(Self { config, conexión })
    }
}

Ejemplo práctico

Veamos un ejemplo más completo que ilustra cómo podríamos implementar un cliente HTTP simple:

struct Cliente {
    base_url: String,
    timeout: std::time::Duration,
}

impl Cliente {
    fn new(base_url: String, timeout_secs: u64) -> Self {
        Self {
            base_url,
            timeout: std::time::Duration::from_secs(timeout_secs),
        }
    }
    
    async fn get(&self, ruta: &str) -> Result<String, Error> {
        let url = format!("{}/{}", self.base_url, ruta);
        self.request("GET", &url).await
    }
    
    async fn post(&self, ruta: &str, datos: &str) -> Result<String, Error> {
        let url = format!("{}/{}", self.base_url, ruta);
        self.request_with_body("POST", &url, datos).await
    }
    
    async fn request(&self, método: &str, url: &str) -> Result<String, Error> {
        println!("Realizando solicitud {} a {}", método, url);
        
        // Simulamos una solicitud HTTP
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        
        Ok(format!("Respuesta de {}", url))
    }
    
    async fn request_with_body(&self, método: &str, url: &str, body: &str) -> Result<String, Error> {
        println!("Realizando solicitud {} a {} con datos: {}", método, url, body);
        
        // Simulamos una solicitud HTTP con cuerpo
        tokio::time::sleep(std::time::Duration::from_millis(150)).await;
        
        Ok(format!("Respuesta de {} con datos procesados", url))
    }
}

// Definimos un tipo de error simple para el ejemplo
#[derive(Debug)]
struct Error(String);

impl From<&str> for Error {
    fn from(msg: &str) -> Self {
        Error(msg.to_string())
    }
}

Este ejemplo muestra cómo podríamos estructurar un cliente HTTP asíncrono utilizando async/await. Aunque es una implementación simplificada, ilustra cómo las funciones asíncronas pueden componerse para crear APIs limpias y expresivas.

La sintaxis async/await nos permite escribir código asíncrono que se lee de manera similar al código síncrono, facilitando enormemente el desarrollo de aplicaciones concurrentes en Rust.

Errores asíncronos

El manejo de errores en código asíncrono es un aspecto fundamental para desarrollar aplicaciones robustas en Rust. Aunque los conceptos básicos son similares al manejo de errores en código síncrono, existen consideraciones especiales cuando trabajamos con futures y la sintaxis async/await.

En Rust, el patrón estándar para manejar errores es mediante el tipo Result<T, E>, y este enfoque se mantiene en el contexto asíncrono. La diferencia principal es que estos valores Result estarán encapsulados dentro de futures.

Propagación de errores con el operador ?

El operador ? funciona de manera similar en funciones asíncronas que en funciones síncronas, permitiendo una propagación elegante de errores:

async fn leer_config(ruta: &str) -> Result<Configuracion, ConfigError> {
    let datos = fs::read_to_string(ruta).await?;
    let config = parsear_config(&datos)?;
    Ok(config)
}

Cuando usamos ? después de una expresión .await, estamos extrayendo el valor de un Result y propagando el error si existe. Esto nos permite escribir código asíncrono conciso que maneja errores de forma elegante.

Tipos de error personalizados para código asíncrono

Es una buena práctica definir tipos de error específicos para nuestras operaciones asíncronas:

#[derive(Debug)]
enum ApiError {
    Conexion(String),
    Timeout(std::time::Duration),
    Respuesta(u16, String),
    Serializacion(serde_json::Error),
    Io(std::io::Error),
}

impl std::error::Error for ApiError {}

impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Conexion(msg) => write!(f, "Error de conexión: {}", msg),
            Self::Timeout(duracion) => write!(f, "Timeout después de {:?}", duracion),
            Self::Respuesta(codigo, msg) => write!(f, "Error HTTP {}: {}", codigo, msg),
            Self::Serializacion(e) => write!(f, "Error de serialización: {}", e),
            Self::Io(e) => write!(f, "Error de E/S: {}", e),
        }
    }
}

// Implementaciones From para facilitar la conversión de errores
impl From<std::io::Error> for ApiError {
    fn from(err: std::io::Error) -> Self {
        Self::Io(err)
    }
}

impl From<serde_json::Error> for ApiError {
    fn from(err: serde_json::Error) -> Self {
        Self::Serializacion(err)
    }
}

Manejo de errores en funciones asíncronas anidadas

Cuando trabajamos con funciones asíncronas anidadas, podemos combinar el operador ? con .await para manejar errores en diferentes niveles:

async fn obtener_usuario(id: u64) -> Result<Usuario, ApiError> {
    let cliente = Cliente::new("https://api.ejemplo.com");
    let respuesta = cliente.get(&format!("/usuarios/{}", id)).await?;
    
    if respuesta.status() != 200 {
        return Err(ApiError::Respuesta(
            respuesta.status(),
            respuesta.text().await?
        ));
    }
    
    let usuario = respuesta.json::<Usuario>().await?;
    Ok(usuario)
}

async fn procesar_perfil_usuario(id: u64) -> Result<PerfilProcesado, ApiError> {
    let usuario = obtener_usuario(id).await?;
    let estadisticas = obtener_estadisticas(id).await?;
    
    Ok(PerfilProcesado {
        nombre: usuario.nombre,
        email: usuario.email,
        actividad: estadisticas.nivel_actividad,
    })
}

Errores en contextos de timeout

Un escenario común en programación asíncrona es establecer timeouts para operaciones que podrían tardar demasiado:

use std::time::Duration;
use futures::future::{self, Future, FutureExt};

async fn operacion_con_timeout<F, T, E>(
    futuro: F,
    timeout: Duration
) -> Result<T, TimeoutError<E>> 
where
    F: Future<Output = Result<T, E>>,
{
    match future::timeout(timeout, futuro).await {
        Ok(resultado) => resultado.map_err(TimeoutError::Operacion),
        Err(_) => Err(TimeoutError::Timeout),
    }
}

#[derive(Debug)]
enum TimeoutError<E> {
    Timeout,
    Operacion(E),
}

// Ejemplo de uso
async fn obtener_datos_con_timeout(url: &str) -> Result<String, TimeoutError<ApiError>> {
    let operacion = obtener_datos(url);
    operacion_con_timeout(operacion, Duration::from_secs(5)).await
}

Manejo de errores en operaciones concurrentes

Cuando ejecutamos múltiples operaciones asíncronas concurrentemente, necesitamos estrategias para manejar los errores que puedan surgir:

async fn procesar_multiples_archivos(
    rutas: &[String]
) -> Result<Vec<Datos>, ProcessError> {
    let mut resultados = Vec::with_capacity(rutas.len());
    let mut errores = Vec::new();
    
    for ruta in rutas {
        match procesar_archivo(ruta).await {
            Ok(datos) => resultados.push(datos),
            Err(e) => errores.push((ruta.clone(), e)),
        }
    }
    
    if !errores.is_empty() {
        return Err(ProcessError::Multiples(errores));
    }
    
    Ok(resultados)
}

#[derive(Debug)]
enum ProcessError {
    Archivo(std::io::Error),
    Formato(String),
    Multiples(Vec<(String, std::io::Error)>),
}

Recuperación de errores asíncronos

En algunos casos, queremos intentar recuperarnos de errores en lugar de simplemente propagarlos:

async fn operacion_con_reintentos<F, Fut, T, E>(
    operacion: F,
    max_intentos: usize,
    backoff: Duration,
) -> Result<T, E>
where
    F: Fn() -> Fut,
    Fut: Future<Output = Result<T, E>>,
    E: std::fmt::Debug,
{
    let mut intentos = 0;
    let mut ultimo_error = None;
    
    while intentos < max_intentos {
        match operacion().await {
            Ok(valor) => return Ok(valor),
            Err(e) => {
                println!("Intento {} falló con error: {:?}", intentos + 1, e);
                ultimo_error = Some(e);
                intentos += 1;
                
                if intentos < max_intentos {
                    tokio::time::sleep(backoff * intentos as u32).await;
                }
            }
        }
    }
    
    Err(ultimo_error.unwrap())
}

// Ejemplo de uso
async fn obtener_datos_resiliente(url: &str) -> Result<String, ApiError> {
    operacion_con_reintentos(
        || async { obtener_datos(url).await },
        3,
        Duration::from_millis(500),
    ).await
}

Contexto de error mejorado

Para proporcionar contexto adicional a los errores asíncronos, podemos implementar un patrón similar al que ofrece la biblioteca anyhow:

struct Context<T, E> {
    inner: Result<T, E>,
    context: Option<String>,
}

impl<T, E: std::fmt::Debug> Context<T, E> {
    fn new(result: Result<T, E>) -> Self {
        Self {
            inner: result,
            context: None,
        }
    }
    
    fn context(mut self, message: impl Into<String>) -> Self {
        self.context = Some(message.into());
        self
    }
    
    fn unwrap(self) -> Result<T, ContextError<E>> {
        match self.inner {
            Ok(value) => Ok(value),
            Err(error) => Err(ContextError {
                error,
                context: self.context,
            }),
        }
    }
}

#[derive(Debug)]
struct ContextError<E> {
    error: E,
    context: Option<String>,
}

// Uso en código asíncrono
async fn cargar_configuracion(ruta: &str) -> Result<Config, ContextError<std::io::Error>> {
    let contenido = Context::new(std::fs::read_to_string(ruta).await)
        .context(format!("Error al leer el archivo de configuración: {}", ruta))
        .unwrap()?;
        
    // Continuar procesando...
    Ok(Config::default())
}

Errores en streams asíncronos

Cuando trabajamos con streams (flujos de datos asíncronos), podemos encontrar diferentes estrategias para manejar errores:

use futures::stream::{self, StreamExt};

async fn procesar_stream_con_errores() -> Result<Vec<u32>, ProcessError> {
    let numeros = stream::iter(1..=10)
        .map(|n| async move {
            if n % 3 == 0 {
                Err(format!("No me gustan los múltiplos de 3: {}", n))
            } else {
                Ok(n * 2)
            }
        })
        .buffer_unordered(5); // Procesar hasta 5 elementos concurrentemente
    
    // Estrategia 1: Recolectar solo los éxitos, ignorar errores
    let solo_exitos: Vec<u32> = numeros
        .filter_map(|r| async { r.ok() })
        .collect()
        .await;
    
    // Estrategia 2: Fallar ante el primer error
    let resultado_todo_o_nada = numeros
        .try_collect::<Vec<u32>>()
        .await;
    
    // Estrategia 3: Recolectar éxitos y errores por separado
    let (exitos, errores): (Vec<u32>, Vec<String>) = numeros
        .partition(|r| async { r.is_ok() })
        .await;
    
    if !errores.is_empty() {
        return Err(ProcessError::Parcial(exitos, errores));
    }
    
    Ok(exitos)
}

Patrones avanzados: fallback asíncrono

Un patrón útil es implementar un mecanismo de fallback para operaciones asíncronas:

async fn con_fallback<T, E, F1, F2>(
    primario: F1,
    secundario: F2
) -> Result<T, E>
where
    F1: Future<Output = Result<T, E>>,
    F2: Future<Output = Result<T, E>>,
{
    match primario.await {
        Ok(valor) => Ok(valor),
        Err(_) => secundario.await,
    }
}

// Ejemplo: intentar cargar configuración de diferentes fuentes
async fn cargar_config() -> Result<Config, ConfigError> {
    con_fallback(
        cargar_config_remota(),
        con_fallback(
            cargar_config_local(),
            cargar_config_predeterminada()
        )
    ).await
}

El manejo adecuado de errores en código asíncrono es esencial para crear aplicaciones robustas y mantenibles. Utilizando las técnicas presentadas, podemos escribir código que no solo sea concurrente y eficiente, sino también resiliente frente a fallos.

Combinación de futures simple

Cuando trabajamos con programación asíncrona en Rust, a menudo necesitamos ejecutar múltiples futures simultáneamente y coordinar sus resultados. En lugar de ejecutar un future tras otro de forma secuencial, podemos combinarlos para aprovechar mejor los recursos del sistema y reducir el tiempo total de ejecución.

Ejecución concurrente con join!

La forma más básica de ejecutar múltiples futures concurrentemente es mediante la macro join!, que ejecuta todos los futures proporcionados en paralelo y espera a que todos se completen:

use futures::join;

async fn obtener_datos() -> Result<String, std::io::Error> {
    // Simulamos obtener datos de alguna fuente
    Ok("datos importantes".to_string())
}

async fn obtener_configuracion() -> Result<String, std::io::Error> {
    // Simulamos obtener configuración
    Ok("configuración del sistema".to_string())
}

async fn inicializar_sistema() -> Result<(), std::io::Error> {
    let (resultado_datos, resultado_config) = join!(
        obtener_datos(),
        obtener_configuracion()
    );
    
    let datos = resultado_datos?;
    let config = resultado_config?;
    
    println!("Sistema inicializado con: {} y {}", datos, config);
    Ok(())
}

La macro join! devuelve una tupla con los resultados de cada future en el mismo orden en que fueron especificados. Una característica importante es que todos los futures se ejecutan concurrentemente, pero la función que contiene el join! no continuará hasta que todos los futures se hayan completado.

Usando try_join! para manejar errores

Cuando trabajamos con futures que devuelven Result, la macro try_join! simplifica el manejo de errores:

use futures::try_join;

async fn proceso_completo() -> Result<(u64, String), ApiError> {
    let (recuento, mensaje) = try_join!(
        obtener_recuento_usuarios(),
        obtener_mensaje_bienvenida()
    )?;
    
    Ok((recuento, mensaje))
}

La diferencia clave es que try_join! devolverá el primer error que ocurra, cortocircuitando la ejecución. Esto es equivalente a usar ? en cada resultado de join!, pero con una sintaxis más concisa.

Selección con select!

Cuando necesitamos responder al primer future que se complete, podemos usar la macro select!:

use futures::select;

async fn timeout(duracion: std::time::Duration) {
    tokio::time::sleep(duracion).await;
}

async fn operacion_con_cancelacion() -> Result<String, &'static str> {
    let operacion = async {
        // Simulamos una operación que toma tiempo
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
        Ok("Operación completada".to_string())
    };
    
    let tiempo_limite = timeout(std::time::Duration::from_secs(1));
    
    futures::select! {
        resultado = operacion.fuse() => resultado,
        _ = tiempo_limite.fuse() => Err("La operación excedió el tiempo límite"),
    }
}

La macro select! ejecuta todos los futures concurrentemente y devuelve el resultado del primero que se complete, cancelando los demás. El método .fuse() es necesario para garantizar que los futures no se ejecuten más de una vez.

Trabajando con colecciones de futures

Para procesar una colección de futures, podemos usar FuturesUnordered:

use futures::{stream, StreamExt};

async fn procesar_ids(ids: Vec<u32>) -> Vec<String> {
    let mut futures = stream::FuturesUnordered::new();
    
    // Añadimos todos los futures a la colección
    for id in ids {
        futures.push(async move {
            // Simulamos procesamiento asíncrono
            tokio::time::sleep(std::time::Duration::from_millis(id as u64 * 10)).await;
            format!("Procesado ID: {}", id)
        });
    }
    
    // Recolectamos los resultados a medida que se completan
    futures.collect().await
}

FuturesUnordered nos permite procesar una cantidad dinámica de futures y obtener sus resultados en el orden en que se completan, no en el orden en que fueron añadidos.

Combinando futures con diferentes tipos de retorno

Cuando necesitamos combinar futures que devuelven diferentes tipos, podemos usar tuplas o estructuras:

async fn obtener_numero() -> u32 {
    42
}

async fn obtener_texto() -> String {
    "Hola mundo".to_string()
}

async fn combinar_diferentes_tipos() -> (u32, String) {
    let (numero, texto) = join!(
        obtener_numero(),
        obtener_texto()
    );
    
    (numero, texto)
}

Ejecución secuencial vs. concurrente

Es importante entender la diferencia entre ejecutar futures secuencialmente y concurrentemente:

async fn secuencial() {
    // Ejecución secuencial: un future después de otro
    let resultado1 = operacion1().await;
    let resultado2 = operacion2().await;
    
    // resultado2 solo comienza después de que resultado1 termina
}

async fn concurrente() {
    // Ejecución concurrente: ambos futures se ejecutan simultáneamente
    let (resultado1, resultado2) = join!(
        operacion1(),
        operacion2()
    );
    
    // Ambas operaciones comenzaron al mismo tiempo
}

Patrones comunes de combinación

Veamos algunos patrones útiles para combinar futures:

  • Mapeo asíncrono: Procesar una colección aplicando una función asíncrona a cada elemento:
async fn procesar_elemento(item: u32) -> String {
    format!("Procesado: {}", item)
}

async fn mapeo_asincrono(items: Vec<u32>) -> Vec<String> {
    let mut resultados = Vec::with_capacity(items.len());
    
    for item in items {
        resultados.push(procesar_elemento(item).await);
    }
    
    resultados
}

// Versión concurrente
async fn mapeo_asincrono_concurrente(items: Vec<u32>) -> Vec<String> {
    let futures = items.into_iter()
        .map(|item| procesar_elemento(item))
        .collect::<Vec<_>>();
    
    futures::future::join_all(futures).await
}
  • Ejecución limitada: Procesar múltiples futures pero limitando la concurrencia:
use futures::stream::{self, StreamExt};

async fn procesar_con_limite(items: Vec<u32>, limite: usize) -> Vec<String> {
    stream::iter(items)
        .map(|item| async move {
            // Simulamos procesamiento que consume recursos
            tokio::time::sleep(std::time::Duration::from_millis(50)).await;
            format!("Resultado: {}", item)
        })
        .buffer_unordered(limite) // Limita la concurrencia
        .collect()
        .await
}
  • Patrón productor-consumidor: Un future genera datos que otros consumen:
use futures::channel::mpsc;
use futures::{SinkExt, StreamExt};

async fn productor_consumidor() {
    let (mut tx, mut rx) = mpsc::channel(10); // Canal con buffer de 10
    
    // Productor
    let productor = async move {
        for i in 0..20 {
            tx.send(i).await.expect("Error al enviar");
            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        }
    };
    
    // Consumidor
    let consumidor = async move {
        while let Some(valor) = rx.next().await {
            println!("Recibido: {}", valor);
        }
    };
    
    // Ejecutamos ambos concurrentemente
    join!(productor, consumidor);
}

Ejemplo práctico: consultas paralelas a una API

Veamos un ejemplo más completo que combina varias técnicas para realizar consultas paralelas a una API:

#[derive(Debug)]
struct Usuario {
    id: u32,
    nombre: String,
}

#[derive(Debug)]
struct Publicacion {
    id: u32,
    titulo: String,
}

async fn obtener_usuario(id: u32) -> Result<Usuario, String> {
    // Simulamos latencia de red
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    
    Ok(Usuario {
        id,
        nombre: format!("Usuario {}", id),
    })
}

async fn obtener_publicaciones(usuario_id: u32) -> Result<Vec<Publicacion>, String> {
    // Simulamos latencia de red
    tokio::time::sleep(std::time::Duration::from_millis(150)).await;
    
    let mut publicaciones = Vec::new();
    for i in 1..=3 {
        publicaciones.push(Publicacion {
            id: i,
            titulo: format!("Publicación {} del usuario {}", i, usuario_id),
        });
    }
    
    Ok(publicaciones)
}

async fn obtener_perfil_completo(id: u32) -> Result<(Usuario, Vec<Publicacion>), String> {
    // Obtenemos usuario y publicaciones concurrentemente
    let (usuario, publicaciones) = try_join!(
        obtener_usuario(id),
        obtener_publicaciones(id)
    )?;
    
    Ok((usuario, publicaciones))
}

async fn obtener_multiples_perfiles(ids: Vec<u32>) -> Vec<Result<(Usuario, Vec<Publicacion>), String>> {
    let futures = ids.into_iter()
        .map(|id| obtener_perfil_completo(id))
        .collect::<Vec<_>>();
    
    futures::future::join_all(futures).await
}

Este ejemplo muestra cómo podemos combinar try_join! para operaciones relacionadas y join_all para procesar múltiples conjuntos de operaciones en paralelo.

La combinación eficiente de futures es una habilidad esencial para escribir código asíncrono de alto rendimiento en Rust. Estas técnicas nos permiten aprovechar al máximo la concurrencia mientras mantenemos un código legible y mantenible.

Aprende Rust online

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.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Async/Await 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 la sintaxis básica de async/await y cómo declarar funciones asíncronas en Rust.
  • Aprender a usar el operador await para suspender y reanudar la ejecución de futures.
  • Conocer las limitaciones y patrones comunes en programación asíncrona con Rust.
  • Entender el manejo de errores en código asíncrono, incluyendo propagación, recuperación y contexto de error.
  • Aprender a combinar múltiples futures para ejecución concurrente y paralela eficiente.