Rust: Asincronía

Descubre la programación asíncrona en Rust con async, await, futures y ejecutores para mejorar el rendimiento y concurrencia de tus aplicaciones.

Aprende Rust GRATIS y certifícate

Asincronía en Rust

La programación asíncrona representa uno de los paradigmas más importantes en el desarrollo moderno, especialmente cuando trabajamos con operaciones que pueden bloquear la ejecución del programa. En Rust, la asincronía se ha diseñado desde cero para ofrecer un rendimiento excepcional sin comprometer la seguridad de memoria que caracteriza al lenguaje.

Fundamentos de la programación asíncrona

La asincronía permite que nuestros programas realicen múltiples tareas de forma concurrente sin necesidad de crear hilos del sistema operativo para cada operación. Esto resulta especialmente valioso cuando trabajamos con operaciones de entrada/salida como lecturas de archivos, peticiones de red o consultas a bases de datos.

En lugar de esperar a que una operación termine antes de continuar con la siguiente, el código asíncrono puede pausar su ejecución, permitir que otras tareas progresen, y luego reanudar cuando los datos estén disponibles. Este enfoque maximiza la utilización de recursos del sistema.

El modelo async/await de Rust

Rust implementa la asincronía mediante las palabras clave async y await. Una función marcada como async no se ejecuta inmediatamente, sino que devuelve un Future que representa una computación que puede completarse en el futuro.

async fn obtener_datos() -> String {
    // Simulamos una operación asíncrona
    tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
    "Datos obtenidos".to_string()
}

El operador await es donde ocurre la magia: pausa la ejecución de la función actual hasta que el Future se complete, pero sin bloquear el hilo de ejecución. Durante esta pausa, el runtime puede ejecutar otras tareas pendientes.

Futures: la base de la asincronía

Los Future son el corazón del sistema asíncrono de Rust. Representan valores que estarán disponibles en algún momento futuro. A diferencia de otros lenguajes, los Future en Rust son "lazy" - no hacen nada hasta que se les hace polling activamente.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

// Un Future personalizado simple
struct MiFuture {
    completado: bool,
}

impl Future for MiFuture {
    type Output = String;
    
    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.completado {
            Poll::Ready("¡Completado!".to_string())
        } else {
            self.completado = true;
            Poll::Pending
        }
    }
}

Ejecutores asíncronos

Los ejecutores (executors) son responsables de hacer polling a los Future y coordinar su ejecución. Rust no incluye un runtime asíncrono en su biblioteca estándar, pero existen varias implementaciones populares como Tokio, async-std y smol.

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

El runtime de Tokio proporciona un planificador de tareas eficiente que puede manejar miles de tareas concurrentes en un número reducido de hilos del sistema operativo.

Manejo de múltiples operaciones asíncronas

Cuando necesitamos coordinar múltiples operaciones asíncronas, Rust ofrece varias estrategias. La macro join! permite ejecutar múltiples Future concurrentemente y esperar a que todos se completen:

use tokio::join;

async fn procesar_datos() {
    let (resultado1, resultado2, resultado3) = join!(
        operacion_asincrona_1(),
        operacion_asincrona_2(),
        operacion_asincrona_3()
    );
    
    println!("Resultados: {}, {}, {}", resultado1, resultado2, resultado3);
}

Para casos donde queremos procesar el primer Future que se complete, utilizamos select!:

use tokio::select;

async fn primera_respuesta() {
    select! {
        resultado = servidor_a() => {
            println!("Servidor A respondió: {}", resultado);
        }
        resultado = servidor_b() => {
            println!("Servidor B respondió: {}", resultado);
        }
    }
}

Streams: secuencias asíncronas

Los Stream extienden el concepto de Future para representar secuencias de valores que llegan de forma asíncrona. Son el equivalente asíncrono de los iteradores:

use tokio_stream::{self as stream, StreamExt};

async fn procesar_stream() {
    let mut stream = stream::iter(vec![1, 2, 3, 4, 5]);
    
    while let Some(valor) = stream.next().await {
        println!("Procesando: {}", valor);
        // Simular procesamiento asíncrono
        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    }
}

Canales asíncronos

Los canales asíncronos facilitan la comunicación entre diferentes partes de nuestro programa asíncrono. Tokio proporciona varios tipos de canales optimizados para diferentes casos de uso:

use tokio::sync::mpsc;

async fn ejemplo_canal() {
    let (tx, mut rx) = mpsc::channel(32);
    
    // Productor
    tokio::spawn(async move {
        for i in 0..10 {
            tx.send(i).await.unwrap();
        }
    });
    
    // Consumidor
    while let Some(mensaje) = rx.recv().await {
        println!("Recibido: {}", mensaje);
    }
}

Gestión de errores en código asíncrono

El manejo de errores en código asíncrono sigue los mismos principios que el código síncrono de Rust, utilizando Result y el operador ?. Sin embargo, la propagación de errores a través de múltiples await requiere atención especial:

use std::error::Error;

async fn operacion_con_errores() -> Result<String, Box<dyn Error>> {
    let datos = obtener_datos_remotos().await?;
    let procesados = procesar_datos(datos).await?;
    let resultado = guardar_resultado(procesados).await?;
    
    Ok(resultado)
}

La composición de operaciones asíncronas que pueden fallar se maneja de forma elegante mediante el operador ?, que propaga automáticamente los errores hacia arriba en la cadena de llamadas.

Empezar curso de Rust

Lecciones de este módulo de Rust

Lecciones de programación del módulo Asincronía del curso de Rust.