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ícateAsincroní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 de Rust
Lecciones de programación del módulo Asincronía del curso de Rust.