async/await en Rust con Tokio: fundamentos del runtime y tareas asíncronas

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 fn devuelve un Future que no hace nada hasta que se ejecuta con .await.
  • #[tokio::main] arranca el runtime multi-hilo; #[tokio::test] para tests.
  • tokio::spawn lanza 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_blocking para trabajo síncrono.
  • Client::clone() en reqwest comparte el pool de conexiones: crea uno y clona.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP