Guía completa de async/await en Rust para desarrolladores
Esta guía cubre los conceptos esenciales y las mejores prácticas para escribir código asíncrono en Rust: modelos de concurrencia, runtimes (Tokio, async-std), futuros, Pin y lifetimes, patrones de rendimiento, manejo de errores y debugging. Incluye ejemplos prácticos y una estructura mínima de proyecto para empezar.
Por qué Rust + async importa
Rust apuesta por concurrencia segura sin data races en tiempo de compilación. Su modelo async es de "zero-cost abstractions": los futuros son estructuras que representan trabajo pendiente; solo cuando se await se avanza el estado. Esto permite alto rendimiento y control fino sobre la memoria y la ejecución.
Conceptos clave
- Future: un valor que representa trabajo que puede completarse más adelante. Implementa
Futurecon un métodopoll. - Pin: garantiza que un futuro no se mueva en memoria (importante si contiene punteros auto-referenciales).
- Runtime: ejecuta las tareas async. Ejemplos: Tokio (más usado, industrial), async-std.
- Send/Sync: para usar
tokio::spawnlas tareas deben serSend + 'staticen el runtime multi-thread. - await: suspende la tarea hasta que el future esté listo. No confundir con threading: await no crea hilos por sí mismo.
Patrones y prácticas comunes
- Usa
spawn_blockingpara trabajo CPU-bound en Tokio; evita bloquear hilos de runtime. - Prefiere
FuturesUnorderedostream::FuturesUnorderedcuando debes ejecutar muchas tareas concurrentes y recolectar resultados sin bloquear. - No pongas
.awaitdentro de unforsi quieres concurrencia: colecta futures y luegoawaittodos concurridamente. - Usa canales acotados (bounded) para backpressure; los canales ilimitados pueden agotar memoria.
- Maneja cancelación con
tokio::select!y timeouts contokio::time::timeout.
Ejemplos prácticos
Cargo.toml mínimo
[package]
name = "async-guide"
version = "0.1.0"
edition = "2021"
[dependencies]
# Tokio runtime, features según necesidades
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
reqwest = { version = "0.11", features = ["json", "gzip", "blocking"] }
anyhow = "1.0"
futures = "0.3"
tracing = "0.1"
tracing-subscriber = "0.3"
main.rs — ejemplo realista
use anyhow::Result;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::Client;
use std::time::Duration;
use tokio::time;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let client = Client::new();
let urls = vec![
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
];
// Ejecutar múltiples requests concurrentemente sin bloquear el hilo
let mut tasks = FuturesUnordered::new();
for url in urls {
let c = client.clone();
tasks.push(tokio::spawn(async move {
// timeout por request
match time::timeout(Duration::from_secs(5), fetch(&c, url)).await {
Ok(Ok(body)) => Ok(body),
Ok(Err(e)) => Err(e),
Err(_) => Err(anyhow::anyhow!("timeout")),
}
}));
}
while let Some(res) = tasks.next().await {
match res {
Ok(Ok(body)) => tracing::info!("Got {} bytes", body.len()),
Ok(Err(e)) => tracing::error!("Task error: {:#}", e),
Err(join_err) => tracing::error!("Join error: {:#}", join_err),
}
}
Ok(())
}
async fn fetch(client: &Client, url: &str) -> Result {
let resp = client.get(url).send().await?.error_for_status()?;
let body = resp.text().await?;
Ok(body)
}
Evitar .await en bucle (antipatrón)
// Antipatrón: se espera secuencialmente
for u in urls.iter() {
let body = fetch(&client, u).await?; // Se hace uno a uno
}
// Correcto: crear futures y ejecutarlos concurrentemente
let futures: Vec<_> = urls.iter().map(|u| fetch(&client, u)).collect();
let results = futures::future::join_all(futures).await;
Implementar tu propio Future (cuando toca)
Rara vez necesitas implementar Future manualmente. Pero cuando lo hagas, recuerda usar Pin y estados finitos (state machine). La mayoría de las veces las combinators y async/await generan el state machine por ti.
Manejo de errores y tipos
- Usa
thiserroroanyhowsegún si necesitas errores tipados o dinámicos. - Propaga errores con
?dentro de funciones async. - Al usar
tokio::spawn, captura errores explícitamente:JoinHandle.>
Rendimiento y optimización
- Prefiere el runtime multi-thread para cargas IO-bound con muchas tasks; usa current_thread para tareas ligeras o tests.
- Reutiliza clientes HTTP y conexiones (ej. reqwest::Client) para aprovechar keep-alive.
- Mide: usa
tokio-consoley flamegraphs. No optimices a ciegas. - Para altas tasas, usa estructuras lock-free o canales con backpressure (tokio::sync::mpsc con capacidad limitada).
Debugging y observabilidad
tracing + tracing-subscriber para spans y logs estructurados. Para problemas en runtime distribuidos, tokio-console (console subscriber) te permite inspeccionar tasks en vivo. Usa timeouts y métricas (prometheus) para detectar latencias.
Ecosistema y crates útiles
- tokio: runtime por excelencia
- async-std: alternativa más "std-like"
- futures: combinators y utilidades
- reqwest: cliente HTTP async
- warp, axum: frameworks web basados en Tokio
- tokio-util, bytes: utilidades para IO eficiente
Estructura mínima de proyecto
async-guide/
├─ Cargo.toml
└─ src/
├─ main.rs
├─ lib.rs # lógica reutilizable
└─ services/
├─ http_client.rs
└─ worker.rs
Separar main.rs (setup, runtime) de la lógica facilita testing y evita dependencias del runtime en módulos puros.
Errores comunes
- No entender Send + 'static al usar
tokio::spawn— causa errores de compilación difíciles. - Bloquear el runtime con operaciones sin usar
spawn_blocking. - Crear demasiadas tareas ligeras (millones) sin backpressure.
Checklist antes de producción
- Probar under load: verificar memory footprint y latencia p99.
- Añadir timeouts y cancellation points.
- Configurar un runtime apropiado y parámetros de worker threads.
- Instrumentar con tracing y métricas.
Consejo avanzado: cuando necesites máxima performance en flujos de eventos, combina FuturesUnordered, canales acotados y evita allocations repetidas; usa Bytes y buffers reciclables. Próximo paso: instrumentar con tokio-console y generar flamegraphs para identificar contención en executor.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación