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ícateSintaxis 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.
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 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.