Guía definitiva de programación asíncrona en Rust con Tokio
Una referencia práctica para entender el modelo async de Rust, evitar errores comunes y construir servicios de red robustos y eficientes con Tokio.
Por qué usar async en Rust
- Escalabilidad: muchas conexiones ligadas a pocos hilos.
- Control preciso de recursos: ownership y tipos ayudan a evitar condiciones de carrera.
- Alto rendimiento: bajo overhead comparado con hilos OS por conexión.
Conceptos clave
- Future: valor perezoso que todavía no terminó; necesita ser
.awaito ejecutado por un executor. - Executor: motor que pollea y ejecuta futures (Tokio es un executor).
- Send/Sync: requisitos para mover futures entre hilos en runtimes multihilo.
- async/await: sintaxis para trabajar libres de callbacks.
Tokio vs async-std (breve)
Tokio es la opción más madura para servicios de red y ofrece un ecosistema amplio (runtime multihilo, utilities, io, sync primitives). async-std es más minimalista y API-compatible con std. Para producción y latencia/throughput alto, Tokio suele ser la elección.
Estructura mínima del proyecto
my-tokio-server/
├─ Cargo.toml
└─ src/
└─ main.rs
Cargo.toml (dependencias recomendadas):
[package]
name = "my-tokio-server"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
Servidor TCP asincrónico completo (explicado)
Características del ejemplo: runtime multihilo, límite de concurrencia con Semaphore, timeout por operación, spawn_blocking para CPU-bound, y cierre ordenado con ctrl_c.
// src/main.rs
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::Semaphore;
use tokio::time::{timeout, Duration};
use tracing::{info, error};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Tracing para logs
tracing_subscriber::fmt::init();
let addr = "127.0.0.1:8080";
let listener = TcpListener::bind(addr).await?;
info!(%addr, "Servidor escuchando");
// Limita número de conexiones simultáneas
let max_conns = 100;
let sem = Arc::new(Semaphore::new(max_conns));
// Ctrl-C graceful shutdown
let shutdown = tokio::spawn(async move {
tokio::signal::ctrl_c().await.expect("failed to listen for ctrl_c");
});
loop {
tokio::select! {
_ = &mut shutdown.clone() => {
info!("Recibido CTRL-C, cerrando aceptador");
break;
}
accept_res = listener.accept() => {
let (socket, peer) = match accept_res {
Ok(s) => s,
Err(e) => { error!(%e, "falló accept"); continue; }
};
let permit = match sem.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => { error!("semaforo cerrado"); continue; }
};
tokio::spawn(handle_connection(socket, peer.to_string(), permit));
}
}
}
info!("Servidor detenido");
Ok(())
}
async fn handle_connection(socket: tokio::net::TcpStream, peer: String, _permit: tokio::sync::OwnedSemaphorePermit) {
info!(%peer, "nueva conexión");
let (r, mut w) = socket.into_split();
let mut reader = BufReader::new(r);
let mut line = String::new();
loop {
line.clear();
// Timeout para lectura: evita conexiones zombis
match timeout(Duration::from_secs(30), reader.read_line(&mut line)).await {
Ok(Ok(0)) => { info!(%peer, "cliente cerró conexión"); break; }
Ok(Ok(_)) => {
let rx = line.trim_end().to_string();
// Emular trabajo CPU-bound con spawn_blocking
let reply = tokio::task::spawn_blocking(move || cpu_work(&rx)).await;
match reply {
Ok(resp) => {
if let Err(e) = w.write_all(resp.as_bytes()).await {
error!(%peer, %e, "error al enviar"); break;
}
}
Err(e) => { error!(%e, "spawn_blocking falló"); break; }
}
}
Ok(Err(e)) => { error!(%peer, %e, "error de io"); break; }
Err(_) => { info!(%peer, "timeout de lectura"); break; }
}
}
info!(%peer, "handler terminado");
}
fn cpu_work(s: &str) -> String {
// Simula trabajo intensivo
std::thread::sleep(std::time::Duration::from_millis(50));
format!("ECO: {}\n", s)
}
Por qué está diseñado así ("por qué")
- Semaphore: evita OOM y backpressure poniendo un techo en conexiones concurrentes.
- timeout en lectura: evita que sockets inactivos queden ocupando recursos.
- spawn_blocking: mantiene el event loop fluido para I/O al sacar trabajo CPU-bound a hilos bloqueantes.
- tokio::select!: permite reaccionar a señales externas (graceful shutdown) sin bloquear la aceptación.
Errores comunes y cómo solucionarlos
- Bloquear el runtime: usar operaciones sincrónicas (p. ej. heavy CPU o llamadas de I/O bloqueantes) directamente dentro de async. Solución:
spawn_blocking. - Sostener locks across .await: no mantener
Mutexo guardas sobre una .await. Solución: reacceder después del .await o usar estructuras de datos diseñadas para async (tokio::sync::Mutex si estrictamente necesario). - Non-Send futures: pasar futures no-Send a un runtime multihilo. Solución: asegúrate que tipos capturados sean Send, o usa runtime single-threaded deliberadamente.
- Canales sin límites: usar canales no acotados para buffering puede crecer sin control. Solución: usar canales acotados y backpressure.
- No cerrar tareas hijas en shutdown: pueden quedar colgadas. Solución: usar JoinSet o señales/cancellation token para coordinar cierre.
Mejores prácticas rápidas
- Prefiere primitivas async de
tokio::syncsobre las de std cuando trabajas en async. - Usa
tracingpara logging estructurado; evita println en producción. - Benchmark y perf: perf, flamegraph, tokio-console para detectar hot spots y contention.
- Define límites (concurrency limits, timeouts, tamaños de buffer) desde el diseño.
Comandos útiles
cargo new my-tokio-server --edition 2021
cd my-tokio-server
# añadir dependencias en Cargo.toml
cargo run --release
Siguientes pasos y consejos avanzados
Prueba a integrar: TLS (rustls), HTTP usando hyper, o instrumentación con tokio-console. Para cargas extremadamente altas, perfila para encontrar contenciones (locks, heap allocs, syscalls). Si tu futuro debe ser Send pero no lo es, revisa qué captura el closure: Rc/RefCell o tipos no-Send suelen ser la causa.
Advertencia: no ignores backpressure ni timeouts en servicios de red; son las causas más comunes de degradación silenciosa en producción.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación