La programación asíncrona en Rust es explícita: necesitas un runtime que ejecute los futures. Tokio es el más usado. A diferencia de otros lenguajes donde async/await es transparente, en Rust tienes control total sobre qué se ejecuta, cuándo y en cuántos hilos, lo que te permite ajustar el rendimiento con precisión.
Futures y el runtime
Un async fn devuelve un Future: una tarea que puede completarse más adelante. El future no hace nada hasta que el runtime lo ejecuta. Tokio provee ese runtime.
// Cargo.toml:
// tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
println!("Empieza");
let resultado = tarea_asincrona().await;
println!("Resultado: {resultado}");
}
async fn tarea_asincrona() -> i32 {
// Simular trabajo sin bloquear el hilo
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
42
}
tokio::spawn: tareas concurrentes
use tokio::task;
#[tokio::main]
async fn main() {
let h1 = task::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
"tarea 1"
});
let h2 = task::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
"tarea 2"
});
// Ambas corren en paralelo; join espera a las dos
let (r1, r2) = tokio::join!(h1, h2);
println!("{}", r1.unwrap()); // tarea 1
println!("{}", r2.unwrap()); // tarea 2
}
Peticiones HTTP paralelas con join!
// reqwest = { version = "0.11", features = ["json"] }
use reqwest::Client;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct Post { id: u32, title: String }
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::new();
let (p1, p2, p3) = tokio::join!(
client.get("https://jsonplaceholder.typicode.com/posts/1").send(),
client.get("https://jsonplaceholder.typicode.com/posts/2").send(),
client.get("https://jsonplaceholder.typicode.com/posts/3").send(),
);
let posts: Vec<Post> = tokio::join!(
p1?.json::<Post>(),
p2?.json::<Post>(),
p3?.json::<Post>(),
).into(); // no compila así; ver patrón correcto abajo
Ok(())
}
// Patrón correcto con try_join!
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = Client::new();
let url = "https://jsonplaceholder.typicode.com/posts/";
let futures: Vec<_> = (1..=5).map(|i| {
let c = client.clone();
tokio::spawn(async move {
c.get(format!("{url}{i}")).send().await?.json::<Post>().await
})
}).collect();
for fut in futures {
let post = fut.await??;
println!("{}: {}", post.id, post.title);
}
Ok(())
}
select!: el primer future que gana
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let tarea = async {
sleep(Duration::from_secs(5)).await;
"completado"
};
let timeout = sleep(Duration::from_secs(1));
tokio::select! {
resultado = tarea => println!("Tarea: {resultado}"),
_ = timeout => println!("Timeout: operación cancelada"),
}
}
Bloqueo vs no bloqueo
#[tokio::main]
async fn main() {
// MAL: bloquea el hilo del runtime
// thread::sleep(Duration::from_secs(1));
// BIEN: cede el control al runtime durante la espera
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// Si necesitas trabajo CPU intensivo, usa spawn_blocking
let resultado = tokio::task::spawn_blocking(|| {
// código que bloquea (IO síncrono, cálculo pesado)
std::thread::sleep(std::time::Duration::from_millis(100));
"listo"
}).await.unwrap();
println!("{resultado}");
}
Resumen
async fndevuelve unFutureque no hace nada hasta que se ejecuta con.await.#[tokio::main]arranca el runtime multi-hilo;#[tokio::test]para tests.tokio::spawnlanza tareas concurrentes;tokio::join!espera a varias a la vez.select!elige el primer future que resuelve; perfecto para timeouts.- Nunca llames a funciones de bloqueo dentro de async; usa
spawn_blockingpara trabajo síncrono. Client::clone()en reqwest comparte el pool de conexiones: crea uno y clona.
