Rust: Asincronía

Rust
Rust

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.

Lecciones de este módulo

Explora todas las lecciones disponibles en Asincronía

Explora más sobre Rust

Descubre más recursos de Rust

Alan Sastre - Autor del curso

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Rust es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.