Programación Concurrente

Tutorial Rust 2026-01

Aprende Rust desde sus fundamentos hasta sus mecanismos de concurrencia: instalación, ownership, borrow checker, tipos, manejo de errores, hilos, canales, sincronización y tareas asíncronas.

1 dueño por valor para razonar sobre memoria
& referencias controladas por el borrow checker
Send tipos que pueden moverse entre hilos de forma segura

Aplicaciones y tendencias actuales

Rust ya no es solo un lenguaje de nicho para sistemas: hoy aparece en seguridad, infraestructura web, sistemas operativos, tooling, WebAssembly y software embebido.

Seguridad

Memoria segura en sistemas reales

Android, Windows y Linux lo exploran o adoptan para reducir clases completas de errores como use-after-free, buffer overflows y data races.

Infraestructura

Proxies, redes y servicios de alto tráfico

Rust encaja bien en servidores, proxies, balanceadores y componentes de red donde importan latencia, eficiencia y control de recursos.

Backend

APIs y microservicios eficientes

Frameworks como Axum, Actix Web y Rocket permiten construir servicios web rápidos con modelos async basados en Tokio.

WebAssembly

Rust en navegador, edge y plugins

Su compilación a WebAssembly lo vuelve atractivo para código portable, extensiones, procesamiento intensivo y runtimes en el borde.

Embebidos

IoT, firmware y tiempo real

El control sin garbage collector y la seguridad del sistema de tipos ayudan en dispositivos con poca memoria y alta exigencia de confiabilidad.

Migración

Convivencia con C y C++

La tendencia no es reescribir todo: Rust se introduce gradualmente en módulos críticos, bibliotecas nuevas y fronteras donde la seguridad paga más.

Para programación concurrente, la tendencia más importante es esta: Rust no elimina la complejidad de coordinar tareas, pero sí obliga a declarar propiedad, acceso compartido y sincronización de manera explícita.

Lo que hace distinto a Rust

Rust empuja errores comunes hacia compilación: uso después de liberar memoria, punteros colgantes, carreras de datos y acceso concurrente sin sincronización.

Memoria

Ownership y movimiento

Cada valor tiene un dueño. Cuando se asigna un String a otra variable, la propiedad se mueve y la variable anterior queda invalidada.

Préstamos

Borrow checker

Las referencias permiten leer o modificar sin transferir propiedad. Muchas referencias inmutables pueden coexistir, pero una mutable exige acceso exclusivo.

Errores

Option y Result

Rust evita depender de valores nulos. Los casos de ausencia o error se modelan como tipos y se resuelven con match.

Tipos

Tipado estático

El compilador infiere tipos cuando puede, pero los parámetros y retornos de funciones se declaran explícitamente para mantener contratos claros.

Modelado

Structs, enums e impl

Los structs agrupan datos, los enums representan estados y los bloques impl agregan métodos al tipo.

Rendimiento

Abstracciones cero costo

Iteradores, closures y tipos expresivos mantienen código de alto nivel sin pagar penalizaciones innecesarias en tiempo de ejecución.

De una idea a un proyecto Rust

El notebook propone instalar Rust con rustup, crear un proyecto con cargo y, para ejemplos asíncronos, sumar Tokio como runtime.

1

Instalar

rustup configura el compilador, Cargo y el entorno base de Rust.

2

Crear

cargo new rust_playground genera la estructura inicial del proyecto.

3

Ejecutar

cargo run compila y ejecuta. cargo test, fmt y clippy ayudan a mantener calidad.

4

Escalar

Crates como rand o tokio amplían el proyecto sin abandonar el flujo de Cargo.

Ejemplos esenciales

Fragmentos compactos inspirados en el notebook para explicar la mecánica mental de Rust antes de entrar a concurrencia.

Ownership move
fn main() {
    let s1 = String::from("Hola");
    let s2 = s1;

    // println!("{}", s1); // error: valor movido
    println!("{}", s2);
}
Borrowing &T
fn longitud(texto: &String) -> usize {
    texto.len()
}

fn main() {
    let saludo = String::from("Hola Rust");
    println!("{}", longitud(&saludo));
    println!("{}", saludo);
}
Pattern matching Option
fn divide(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn main() {
    match divide(10.0, 2.0) {
        Some(r) => println!("Resultado: {}", r),
        None => println!("Division por cero"),
    }
}
Closures e iteradores zero-cost
fn main() {
    let numeros = vec![1, 2, 3, 4, 5];

    let suma: i32 = numeros
        .iter()
        .filter(|x| *x % 2 == 0)
        .map(|x| x * 2)
        .sum();

    println!("{}", suma);
}
En Google Colab, el notebook define una cell magic %%rust para escribir el contenido de una celda en src/main.rs y ejecutarlo con cargo run. Es práctico para clase, aunque la sintaxis se vea como Python dentro de Colab.

Concurrencia sin miedo

Rust combina hilos del sistema, comunicación por canales, sincronización con Arc/Mutex y asincronía con async/await.

Ejecución secuencial en Rust
La ejecución secuencial concentra el trabajo en una ruta de control.
Ejecución concurrente en Rust
La ejecución concurrente reparte tareas y exige reglas claras de propiedad y sincronización.
Threads

thread::spawn

Un closure con move transfiere datos al hilo. join espera su finalización y evita que el programa termine antes.

Estado compartido

Arc<Mutex<T>>

Arc comparte propiedad entre hilos y Mutex garantiza acceso exclusivo al dato protegido.

Mensajes

mpsc::channel

Los sensores concurrentes del notebook envían lecturas de temperatura y humedad a un receptor central.

Patrones de concurrencia en código

Estos ejemplos muestran tres estilos complementarios: compartir memoria, pasar mensajes y trabajar con tareas asíncronas.

Hilo con propiedad movida std::thread
use std::thread;

fn main() {
    let numeros = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("{:?}", numeros);
    });

    handle.join().unwrap();
}
Lectores y escritor Arc + Mutex
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let dato = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for id in 0..3 {
        let copia = Arc::clone(&dato);
        handles.push(thread::spawn(move || {
            let valor = copia.lock().unwrap();
            println!("lector {} ve {}", id, *valor);
        }));
    }

    for h in handles { h.join().unwrap(); }
}
Sensores concurrentes mpsc
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    let tx_temp = tx.clone();
    thread::spawn(move || {
        for temp in [22, 24, 27] {
            tx_temp.send(("temperatura", temp)).unwrap();
        }
    });

    thread::spawn(move || {
        for hum in [65, 62, 70] {
            tx.send(("humedad", hum)).unwrap();
        }
    });

    for lectura in rx.iter().take(6) {
        println!("{:?}", lectura);
    }
}
Clientes masivos Tokio
use tokio::time::{sleep, Duration};

async fn cliente(id: u32) {
    println!("Cliente {} conectado", id);
    sleep(Duration::from_millis(100)).await;
    println!("Cliente {} desconectado", id);
}

#[tokio::main]
async fn main() {
    for id in 0..10 {
        tokio::spawn(cliente(id));
    }
}

Rust también es diseño de tipos

La concurrencia segura no aparece al final: depende de funciones, tipos personalizados y traits que expresan qué puede moverse o compartirse.

Logo de Rust en el material de concurrencia

Send

Un tipo que implementa Send puede transferir su propiedad a otro hilo.

Unidad básica de concurrencia

Sync

Un tipo que implementa Sync permite compartir referencias entre hilos cuando hacerlo es seguro.

Procesos concurrentes

Estado explícito

enum, struct e impl ayudan a modelar estados, mensajes y responsabilidades.

Qué herramienta usar y cuándo

Una parte valiosa de Rust es que no obliga a un solo estilo de concurrencia. El diseño depende de si quieres aislar tareas, compartir estado o coordinar muchas esperas de I/O.

T

Usa hilos cuando el trabajo sea CPU-bound

Para cálculo pesado o tareas independientes, std::thread permite repartir trabajo real entre núcleos. Combínalo con move para transferir propiedad de forma explícita.

M

Usa Arc<Mutex<T>> cuando haya estado compartido

El contador, una cache o una estructura global necesitan una regla clara de acceso. Arc comparte propiedad y Mutex evita escrituras simultáneas peligrosas.

C

Usa channels cuando el modelo sea productor-consumidor

Los canales son ideales para sensores, pipelines y tareas que envían eventos. Separan a quien produce datos de quien decide qué hacer con ellos.

A

Usa async cuando haya muchas esperas

Con Tokio, miles de conexiones o solicitudes pueden avanzar sin crear miles de hilos. Es fuerte para red, archivos, temporizadores y servidores concurrentes.

Regla de oro: si el compilador se queja en concurrencia, casi siempre está mostrando una decisión de diseño que falta. Revisa quién posee el dato, quién lo presta, y si realmente debe compartirse.