rust-async-await-introduccion

Async y await en Rust: bases de la programación asíncrona

  • 3 min

La programación asíncrona permite suspender tareas sin bloquear el hilo mientras esperan.

Esto es muy útil cuando el programa pasa mucho tiempo esperando cosas externas: red, disco, bases de datos, temporizadores, APIs… En vez de quedarse mirando al infinito, Rust puede aprovechar ese tiempo para avanzar con otras tareas.

El problema de esperar

Supongamos que hacemos dos peticiones HTTP y cada una tarda un segundo.

Si las ejecutamos de forma secuencial, el programa espera la primera y luego espera la segunda. Total: dos segundos.

let a = pedir_a().await;
let b = pedir_b().await;
Copied!

Con async podemos organizar el trabajo para que varias operaciones avancen a la vez, siempre que estén esperando a E/S y no quemando CPU.

Async no hace que una suma sea más rápida. Si tu tarea es calcular números sin parar, necesitas paralelismo real. Async brilla cuando hay esperas.

Qué hace async

Cuando marcamos una función como async, la función no devuelve directamente el resultado. Devuelve un Future.

async fn saludar() -> String {
    String::from("Hola")
}
Copied!

Aunque parezca que devuelve String, realmente devuelve algo que representa un resultado que estará disponible más adelante. Ese algo es un Future.

Qué hace .await

Para obtener el resultado de un Future, usamos .await.

async fn saludar() -> String {
    String::from("Hola")
}

#[tokio::main]
async fn main() {
    let mensaje = saludar().await;
    println!("{}", mensaje);
}
Copied!

Cuando el futuro devuelve Pending, .await suspende esa tarea y permite que el runtime ejecute otras. Esto no convierte en asíncrona una operación bloqueante: una llamada síncrona lenta dentro del futuro seguiría bloqueando el hilo.

Necesitamos un runtime

Rust define Future, async y .await, pero no trae un runtime asíncrono completo en la librería estándar.

Para ejecutar código async necesitamos un ejecutor, normalmente proporcionado por un runtime como Tokio o async-std.

Tokio es una de las opciones más extendidas:

cargo add tokio --features full
Copied!

Y podemos usar #[tokio::main] para arrancar el runtime:

use tokio::time::{sleep, Duration};

async fn tarea_lenta() {
    sleep(Duration::from_secs(1)).await;
    println!("Terminé");
}

#[tokio::main]
async fn main() {
    tarea_lenta().await;
}
Copied!

Los futuros son perezosos

Un detalle importante: crear un Future no lo ejecuta automáticamente.

async fn tarea() {
    println!("Ejecutando tarea");
}

#[tokio::main]
async fn main() {
    let futuro = tarea();

    // Aquí todavía no se ha impreso nada

    futuro.await;
}
Copied!

El futuro empieza a avanzar cuando alguien lo espera con .await o cuando lo entrega al runtime con herramientas como tokio::spawn.

Async no es paralelismo automático

Esto es importante para no llevarnos un chasco.

tarea_1().await;
tarea_2().await;
Copied!

Esto ejecuta tarea_1, espera a que termine, y luego ejecuta tarea_2. Es asíncrono, sí, pero sigue siendo secuencial.

Para ejecutar varias tareas concurrentemente usaremos tokio::join!, tokio::spawn o tokio::select!. Eso lo veremos en la próxima entrada.