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;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")
}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);
}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 fullY 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;
}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;
}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;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.