study.plynte.com
@franguh
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)

08 · Async y concurrencia#

Objetivo#

Al terminar esta lección podrás explicar con precisión de ingeniería cómo el compilador de Rust transforma funciones asíncronas (async/await) en máquinas de estados de cero costo (zero-cost state machines), distinguir rigurosamente la concurrencia cooperativa del paralelismo de hardware, y diagnosticar las causas profundas de la inanición de hilos (thread starvation). Dominarás las consecuencias sutiles pero críticas de retener cerraduras en memoria (locks) a través de puntos de suspensión, y comprenderás por qué la cancelación de un futuro en Rust jamás revierte mágicamente una mutación enviada a través de la red a una base de datos o servicio externo.


Antes de empezar#

  1. Completa las lecciones previas sobre ownership, préstamos, Result, Actix Web y transacciones relacionales.
  2. Esta lección divide sus ejemplos de código en dos categorías explícitas: - Mecánica interna con la Biblioteca Estándar (std::future::Future): Código Rust estándar autónomo que modela el trait Future, el método poll y la estructura Waker, totalmente ejecutable y verificable con rustc --test. - Patrones asíncronos con Runtimes (Tokio / Actix): Fragmentos respaldados por dependencias externas para servidores en producción, etiquetados como crate-backed.
  3. Si encuentras algún concepto técnico desconocido, consulta el glosario técnico.

Diccionario de conceptos de la lección (Glosario desde cero)#

1. Concurrencia vs Paralelismo#

  • Intuición cotidiana:
  • Concurrencia: Un chef en una cocina pequeña preparando sopa y pastel al mismo tiempo. Pone la sopa a hervir a fuego lento (espera pasiva) y, mientras hierve, bate la masa del pastel. Hay un solo chef intercalando dos tareas según la disponibilidad de recursos.
  • Paralelismo: Dos chefs trabajando hombro a hombro en dos estufas independientes al mismo segundo exacto.
  • Definición técnica precisa:
  • Concurrencia: Propiedad arquitectónica del software donde múltiples secuencias de instrucciones hacen progreso durante intervalos de tiempo superpuestos mediante intercalado (interleaving). Puede ocurrir sobre un único núcleo físico de CPU.
  • Paralelismo: Ejecución física y simultánea de múltiples instrucciones en el mismo instante de tiempo sobre múltiples núcleos o procesadores de hardware independientes. En Rust, escribir async proporciona concurrencia; el paralelismo dependerá de si el runtime asigna esas tareas a diferentes hilos del sistema operativo sobre múltiples núcleos físicos.
  • Ejemplo conceptual:

Una aplicación con 10,000 conexiones WebSocket abiertas en un servidor de 2 núcleos tiene alta concurrencia; en cada ciclo de reloj de la CPU, a lo sumo 2 instrucciones se ejecutan en paralelo.

  • Concepción errónea: Creer que añadir la palabra clave async hace que tu código corra "el doble de rápido en paralelo". Si el código realiza cálculos matemáticos pesados en la CPU sin esperar I/O, async solo añade sobrecarga de sincronización sin aportar ningún beneficio.

2. Futuro (Future trait, método poll y Waker)#

  • Intuición cotidiana: Un buscapersonas (pager) vibrador que te entregan en un restaurante lleno: no te entregan la comida de inmediato; te dan el dispositivo y te dicen "espera sentado; cuando el plato esté cocinado, este aparato vibrará para avisarte que vuelvas al mostrador a recogerlo".
  • Definición técnica precisa:
  • Future: Trait fundamental de la biblioteca estándar de Rust (std::future::Future) que representa un valor diferido que puede no haber completado su cálculo.
  • Método poll: Firma fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>. Es un modelo de avance basado en demanda (poll-based o pull-based). El futuro no hace nada por sí solo si nadie lo sondea (futures in Rust do nothing unless polled). Retorna Poll::Ready(valor) si concluyó, o Poll::Pending si aún espera un evento externo.
  • Waker: Puntero a una función callback dentro de Context. Cuando el evento físico de red o disco ocurre, el sistema operativo o reactor invoca waker.wake(), notificando al executor que esa tarea específica está lista para ser sondeada nuevamente con poll.
  • Ejemplo en Rust estándar:
RUST
  use std::task::Poll;
  // Poll::Pending -> La operación no está lista aún
  // Poll::Ready(data) -> Operación completada con éxito
  • Concepción errónea: Pensar que los futuros en Rust son como las Promises de JavaScript. En JavaScript, una Promise comienza a ejecutarse en segundo plano inmediatamente al ser instanciada (push-based). En Rust, un futuro es una estructura inerte en memoria: si no le aplicas .await o no lo envías a un executor, jamás se ejecutará ni una sola línea de su código.

3. Ejecutor (Executor) y Reactor de E/S (I/O Reactor)#

  • Intuición cotidiana:
  • Reactor: La torre de control de un aeropuerto: vigila el radar y detecta cuándo un avión solicita pista para aterrizar.
  • Ejecutor: El equipo de pista y pasarela: cuando la torre avisa que el vuelo llegó, los operarios conectan la pasarela, abren las compuertas y desembarcan a los pasajeros.
  • Definición técnica precisa:
  • *Reactor de E/S (I/O Reactor):* Componente de bajo nivel que interactúa con las interfaces de multiplexación de eventos del kernel del sistema operativo (epoll en Linux, kqueue en macOS/BSD, IOCP en Windows). Registra descriptores de archivo / sockets y duerme al hilo hasta que el hardware notifica que hay bytes listos para lectura o escritura.
  • *Ejecutor (Executor): Componente del runtime (como el planificador de Tokio) que mantiene una cola de tareas listas (run queue*). Cuando el reactor despierta una tarea mediante su Waker, el ejecutor extrae la tarea de la cola y le invoca el método poll.
  • Ejemplo conceptual:

Tokio combina un reactor basado en la biblioteca mio con un ejecutor multihilo con robo de trabajo (work-stealing scheduler).

  • Concepción errónea: Confundir el lenguaje Rust con su runtime. Rust deliberadamente no incluye un runtime asíncrono en su biblioteca estándar para permitir que el lenguaje funcione en sistemas embebidos, kernels y microcontroladores donde Tokio sería inviable.

4. Tarea asíncrona (Task) y máquina de estados de async/await#

  • Intuición cotidiana: Un juego de mesa con casillas numeradas: cuando te toca el turno, avanzas hasta la casilla "espera el turno del rival", anotas en tu libreta en qué casilla quedaste y le pasas los dados al siguiente jugador. Cuando vuelve a ser tu turno, consultas tu libreta y continúas exactamente desde esa casilla.
  • Definición técnica precisa: Unidad independiente de ejecución asíncrona gestionada por el runtime (el equivalente a un "hilo ligero" o green thread en espacio de usuario). El compilador de Rust (rustc) transforma automáticamente cada bloque o función async fn en una máquina de estados representada internamente por un enum anónimo. Cada punto donde aparece la palabra clave .await se convierte en una variante del enum que almacena todas las variables locales vivas en ese instante.
  • Ejemplo visual de desazucarado sintáctico:
RUST
  // Lo que escribes:
  async fn workflow() {
      step_one().await;
      step_two().await;
  }

  // En lo que el compilador lo convierte conceptualmente:
  enum WorkflowStateMachine {
      State0,                         // Al inicio
      WaitingOnStepOne(StepOneFuture),// Pausado en el primer .await
      WaitingOnStepTwo(StepTwoFuture),// Pausado en el segundo .await
      Done,
  }
  • Concepción errónea: Pensar que crear una tarea con tokio::spawn asigna un hilo nativo del sistema operativo con su propia pila fija de 2 MB a 8 MB. Una tarea de Tokio es una simple estructura asignada en el montículo (heap) de apenas unos cientos de bytes de tamaño.

5. Multitarea cooperativa vs apropiativa (Preemptive)#

  • Intuición cotidiana:
  • Apropiativa (Preemptive): Un maestro de escuela con silbato: cada 5 minutos hace sonar el silbato y le quita el turno al alumno que está hablando para darle la palabra a otro, sin importar si el primero quería seguir. (Así gestiona el sistema operativo los hilos nativos).
  • Cooperativa: Una ronda de conversación entre caballeros educados: cada quien habla y voluntariamente dice "he terminado mi punto, cedo la palabra" (mediante .await). Si un orador grosero decide no callarse jamás, nadie más puede hablar en esa mesa.
  • Definición técnica precisa:
  • *Multitarea apropiativa (Preemptive Multitasking): El planificador del sistema operativo interrumpe forzosamente un hilo mediante interrupciones de reloj de hardware (timer interrupts) para alternar entre procesos, garantizando justicia (fairness*).
  • *Multitarea cooperativa (Cooperative Multitasking): Modelo de ejecución de Rust async donde una tarea conserva el control del hilo de trabajo (worker thread*) hasta que voluntariamente cede la ejecución retornando Poll::Pending en un punto de suspensión .await.
  • Ejemplo crítico:

Si escribes un bucle infinito loop { /* cómputo pesado */ } sin ningún .await dentro de una función async, la tarea secuestra permanentemente ese worker thread de Tokio. Ninguna otra tarea asignada a ese hilo podrá ejecutarse.

  • Concepción errónea: Creer que el runtime de Tokio puede "interrumpir" una función asíncrona que está ejecutando un algoritmo intensivo de cifrado o procesamiento de imágenes. En Rust async, si tú no cedes el control con .await, nadie te lo puede quitar.

6. Inanición de hilo (Thread Starvation)#

  • Intuición cotidiana: En un banco con un solo cajero, el cliente al frente de la fila saca una bolsa con 500,000 monedas de 1 centavo y le pide al cajero que las cuente a mano una por una. Las 200 personas detrás en la fila sufren retrasos extremos y pierden sus citas aunque sus trámites duraban 10 segundos.
  • Definición técnica precisa: Degradación severa del sistema donde una o varias tareas asíncronas listas para ejecutarse no reciben tiempo de CPU porque un hilo de trabajo (worker thread) está bloqueado por una operación síncrona de larga duración (std::thread::sleep, llamada a API C bloqueante, cómputo masivo de CPU o contención de candados en memoria).
  • Ejemplo en código patológico:
RUST
  async fn bad_handler() {
      // PROHIBIDO en hilos de worker async: congela el worker completo
      std::thread::sleep(std::time::Duration::from_secs(5));
  }
  • Concepción errónea: Pensar que como la máquina tiene 8 núcleos, "no importa bloquear un hilo un ratito". Si 8 clientes concurrentes ejecutan esa función síncrona, los 8 hilos del pool de Tokio quedan congelados: el servidor web dejará de responder pings de salud y todas las demás solicitudes colapsarán con timeouts.

7. Traits marcadores Send y Sync#

  • Intuición cotidiana:
  • Send: Un pasaporte internacional: certifica que el objeto tiene permitido viajar en avión de un país (hilo) a otro país (hilo).
  • Sync: Un libro en una biblioteca pública con vitrina de cristal: certifica que varios lectores pueden mirarlo al mismo tiempo desde distintos ángulos sin destruirlo.
  • Definición técnica precisa:
  • Send: Trait marcador que indica que la propiedad del tipo puede ser transferida a través de las fronteras de hilos del sistema operativo de manera segura. Si un tipo T es Send, mover una variable de tipo T a otro hilo no causará carreras de datos (data races).
  • Sync: Trait marcador que indica que es seguro compartir referencias inmutables (&T) del tipo entre múltiples hilos simultáneamente. Regla de oro de Rust: T es Sync si y solo si &T es Send.
  • Ejemplo en tipos primitivos:
  • Rc<T> no es Send ni Sync porque su contador de referencias interno no es atómico.
  • Arc<T> sí es Send y Sync (si T es Send + Sync) porque utiliza operaciones atómicas de hardware (atomic fetch-and-add).
  • Concepción errónea: Creer que debes implementar Send y Sync manualmente con unsafe. El compilador de Rust los deduce automáticamente para tus structs si todos sus campos miembros implementan Send y Sync.

8. std::sync::Mutex vs tokio::sync::Mutex#

  • Intuición cotidiana:
  • std::sync::Mutex: Una cabina telefónica tradicional con candado: si está ocupada, te quedas parado físicamente en la acera frente a la puerta congelándote de frío hasta que la persona salga.
  • tokio::sync::Mutex: Un sistema de turnos de restaurante por pantalla: si la mesa está ocupada, tomas un ticket con número, te vas a pasear al centro comercial y, cuando tu mesa se desocupa, te envían una notificación al teléfono.
  • Definición técnica precisa:
  • std::sync::Mutex: Primitiva de sincronización provista por el sistema operativo (mediante llamadas futex en Linux o SRWLock en Windows). Si el candado está ocupado, duerme el hilo nativo completo del SO. Si se retiene a través de un punto .await, el compilador rechaza el código si el executor exige Send (porque MutexGuard no es Send).
  • tokio::sync::Mutex: Primitiva de sincronización asíncrona a nivel de espacio de usuario. Si el candado está ocupado, la tarea se suspende cooperativamente retornando Poll::Pending y cede el worker thread a otras tareas. Su guardia MutexGuard sí implementa Send.
  • Regla de oro de decisión:
  • Usa std::sync::Mutex para proteger datos en memoria en operaciones que duren menos de unos pocos microsegundos y cuyo guardia se destruya antes de llegar a cualquier .await.
  • Usa tokio::sync::Mutex únicamente si es estrictamente inevitable retener el candado a través de una llamada .await de E/S.
  • Concepción errónea: Sustituir ciegamente todos los mutex de la aplicación por tokio::sync::Mutex. El mutex asíncrono tiene una penalización de rendimiento notablemente mayor (asignación de estructuras de sincronización, colas de wakers) en comparación con el mutex estándar del SO para secciones críticas cortas en RAM.

9. Cancelación de futuros (Drop) y seguridad ante cancelación (Cancel Safety)#

  • Intuición cotidiana: Si pides una pizza por teléfono y a los 5 minutos decides colgar el teléfono y salir de casa, el restaurante no sabe que colgaste: el pizzero cocinará la pizza y tocará a tu puerta vacía. Cancelar tu llamada no deshizo la pizza en el horno.
  • Definición técnica precisa:
  • Cancelación de futuros: En Rust, una tarea asíncrona se cancela simplemente dejando de sondearla y ejecutando su destructor (drop(future)). No hay hilos interrumpidos ni señales de aborto forzadas.
  • *Seguridad ante cancelación (Cancel Safety):* Propiedad de una función asíncrona donde, si su futuro es destruido (dropped) en cualquier punto intermedio de .await, el sistema no queda en un estado corrupto, inconsistente o con recursos huérfanos.
  • Ejemplo en persistencia:

Si un handler ejecuta un INSERT en la base de datos a través del socket de red y el cliente HTTP cierra la pestaña del navegador inmediatamente después: Tokio cancela la tarea local destruyendo el futuro del handler. Sin embargo, los bytes del INSERT ya viajaron por el cable TCP y PostgreSQL ejecutará el commit. El registro fue creado, pero el cliente jamás recibió la respuesta.

  • Concepción errónea: Asumir que cancelar una función async revierte las transacciones o peticiones remotas que ya fueron emitidas por la red.

Modelo mental y arquitectura profunda#

La desazucaración de async fn en máquinas de estados#

Comprender la transformación interna del compilador desmitifica el funcionamiento de await:

DIAGRAMA MERMAID
stateDiagram-v2
    direction LR
    [*] --> State0: Inicia invocación
    State0 --> WaitingStep1: Inicia step_one() y retorna Poll::Pending
    WaitingStep1 --> State1: Waker notifica. Avanza al recibir Poll::Ready
    State1 --> WaitingStep2: Inicia step_two() y retorna Poll::Pending
    WaitingStep2 --> Done: Waker notifica. Avanza al recibir Poll::Ready
    Done --> [*]

Cuando compilas una función asíncrona como:

RUST
async fn process_order(order_id: u64) -> Result<(), Error> {
    let auth = check_auth().await?;
    let inventory = reserve_stock(order_id).await?;
    persist(auth, inventory).await?;
    Ok(())
}

El compilador genera un enum donde:

  1. Las variables locales vivas que deben sobrevivir entre llamadas a .await (como auth) se convierten en campos de las variantes del enum.
  2. Cada invocación a .await genera una rama en la función poll(Pin<&mut Self>, &mut Context).
  3. El tamaño en memoria de este enum en el stack o en el heap es la envolvente máxima de los datos vivos en cada suspensión. Si tienes variables pesadas vivas a través de .await, el tamaño en bytes de tu futuro aumentará considerablemente.

Los dos fallos de sostener un std::sync::MutexGuard a través de .await#

Uno de los errores más graves en sistemas concurrentes con Rust es cruzar un punto de suspensión mientras se retiene el bloqueo de un mutex síncrono. Esta acción genera dos fallos independientes según la arquitectura del runtime:

DIAGRAMA MERMAID
flowchart TD
    Lock[Adquiere std::sync::MutexGuard] --> CrossAwait{Intenta cruzar .await}
    
    CrossAwait -->|Runtime Multihilo Tokio| FailCompile[Fallo 1: Error de Compilación E0277]
    FailCompile --> ExplainCompile["MutexGuard NO implementa Send. El futuro no puede ser movido entre hilos workers."]
    
    CrossAwait -->|Runtime Unihilo / LocalSet| FailRuntime[Fallo 2: Interbloqueo / Inanición en Ejecución]
    FailRuntime --> ExplainRuntime["El worker thread se suspende reteniendo el lock del SO. Otra tarea en el mismo hilo intenta tomar el lock y congela el proceso para siempre."]
  1. Fallo en compilación (Runtime multihilo con robo de trabajo): Tokio distribuye tareas entre múltiples hilos de trabajo. Cuando una tarea se suspende en un .await y posteriormente es despertada por su waker, puede ser reanudada en un hilo de trabajo del sistema operativo diferente al que inició la ejecución. Las especificaciones POSIX y de sistemas operativos exigen que un mutex estándar sea liberado por el mismo hilo de SO que lo adquirió. Por esta razón, la biblioteca estándar de Rust marca explícitamente a std::sync::MutexGuard como !Send (no puede transferirse entre hilos). Si intentas mantener el guard a través de un .await, el compilador de Rust detectará que el futuro generado no implementa Send y emitirá el error error[E0277]: MutexGuard cannot be sent between threads safely.
  2. Fallo en tiempo de ejecución (Interbloqueo / Inanición en runtimes unihilo): Si se burla la comprobación utilizando un runtime unihilo (tokio::task::LocalSet), el código compilará pero causará una catástrofe en tiempo de ejecución: la tarea A toma el mutex del SO y se suspende esperando un evento de red. Mientras espera, el hilo único del worker ejecuta la tarea B. Si la tarea B intenta adquirir el mismo mutex, invocará la llamada bloqueante del sistema operativo. Como la tarea A no puede reanudarse hasta que la tarea B libere la CPU, el hilo entra en un *interbloqueo permanente (deadlock)*.

La solución arquitectónica: Reducción drástica del alcance del lock#

RUST
// FORMA INCORRECTA: El guard cruza la espera de red
// let guard = state.lock().unwrap();
// let data = fetch_from_network().await?; // ERROR !Send
// guard.update(data);

// FORMA CORRECTA: Sección crítica microscópica en memoria
let initial_config = {
    let guard = state.lock().unwrap();
    guard.get_config().clone() // Copia rápida de datos mínimos
}; // <--- El guard se destruye y el lock se LIBERA aquí mismo

// Espera de red asíncrona SIN retener ningún lock
let data = fetch_from_network(&initial_config).await?;

// Vuelve a adquirir el lock brevemente para escribir el resultado
{
    let mut guard = state.lock().unwrap();
    guard.apply_update(data);
} // <--- Liberado inmediatamente

Diagrama#

DIAGRAMA MERMAID
flowchart TD
    A[Task ejecuta] --> B{¿Operación lista?}
    B -->|sí| C[Task avanza]
    B -->|no| D[Task cede worker]
    D --> E[Otra task lista avanza]
    E --> F[Operación notifica progreso]
    F --> A
    C --> G[Completa o propaga error]

Diagrama estático del tema
Diagrama estático del tema
Fuente editable: Mermaid.

La multitarea cooperativa maximiza la densidad de conexiones concurrentes por máquina física siempre y cuando las tareas cedan el control en cada punto de espera de E/S.


Ejemplo razonado#

Componente 1: Máquina de estados autónoma en Rust Estándar (std)#

El siguiente fragmento demuestra el funcionamiento de un futuro manual basado en sondeos (poll-based), sin necesidad de instalar Tokio ni bibliotecas externas. Puedes guardarlo como manual_future.rs y verificarlo con rustc --test manual_future.rs:

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

// Contract: A custom future that counts down from N to 0 across successive polls.
// Demonstrates how cooperative multitasking yields control via Poll::Pending.
pub struct CountdownFuture {
    remaining_steps: u32,
}

impl CountdownFuture {
    pub fn new(steps: u32) -> Self {
        Self { remaining_steps: steps }
    }
}

impl Future for CountdownFuture {
    type Output = &'static str;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.remaining_steps == 0 {
            Poll::Ready("countdown completed")
        } else {
            self.remaining_steps -= 1;
            // Notify the executor that this future is ready to be polled again
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;
    use std::task::{Wake, Waker};

    struct TestWaker;
    impl Wake for TestWaker {
        fn wake(self: Arc<Self>) {}
    }

    #[test]
    fn simulates_executor_polling_cycle() {
        let waker = Waker::from(Arc::new(TestWaker));
        let mut cx = Context::from_waker(&waker);

        let mut future = CountdownFuture::new(2);
        let mut pinned = Pin::new(&mut future);

        // First poll: decrements to 1, returns Pending
        assert_eq!(pinned.as_mut().poll(&mut cx), Poll::Pending);

        // Second poll: decrements to 0, returns Pending
        assert_eq!(pinned.as_mut().poll(&mut cx), Poll::Pending);

        // Third poll: counter is 0, returns Ready
        assert_eq!(
            pinned.as_mut().poll(&mut cx),
            Poll::Ready("countdown completed")
        );
    }
}

Por qué este diseño es robusto:

  • Expone con absoluta transparencia cómo funciona el contrato entre el compilador, el futuro y el waker sin magias sintácticas.
  • Ilustra que el avance de la computación es un proceso estrictamente determinista guiado por llamadas a poll.

Componente 2: Aislamiento de trabajo intensivo de CPU (spawn_blocking) en Tokio#

En aplicaciones web reales (como cálculo de hashes criptográficos Argon2, compresión de imágenes o parseo de PDFs), el trabajo no debe ejecutarse en el pool de hilos asíncronos:

RUST
// Crate-backed: Requires tokio = { version = "1", features = ["full"] }
use tokio::task;

pub async fn hash_password_handler(password: String) -> Result<String, String> {
    // Correct architecture: Offload heavy CPU work to a dedicated OS thread pool
    let hash = task::spawn_blocking(move || {
        // This runs on a separate thread pool dedicated to blocking operations.
        // The async worker threads remain 100% free to handle incoming HTTP requests!
        compute_argon2_heavy_hash(&password)
    })
    .await
    .map_err(|join_err| format!("Task execution failed: {join_err}"))?;

    Ok(hash)
}

fn compute_argon2_heavy_hash(input: &str) -> String {
    // Simulates expensive cryptographic calculation
    format!("hash_of_{input}")
}

Práctica independiente#

Diseña una simulación y análisis de concurrencia para un servicio de auditoría transaccional:

Requerimientos de la práctica:#

  1. Línea de tiempo de concurrencia e intercalado: - Dibuja un diagrama de tiempos con dos solicitudes entrantes simultáneas, Solicitud A y Solicitud B, gestionadas por un único hilo de trabajo (single-worker pool). - Solicitud A: - t=0 a t=2 ms: Ejecución de CPU (deserialización de JSON y validación). - t=2 a t=10 ms: Espera de I/O de red (consulta a base de datos PostgreSQL remota). - t=10 a t=12 ms: Generación de respuesta HTTP. - Solicitud B llega en t=1 ms. - Pregunta de predicción: Indica en qué milisegundos exactos el worker thread ejecuta la deserialización de B, cuándo B espera su I/O y cuándo concluyen ambas solicitudes.
  2. *Escenario de desconexión y cancelación (Drop):* - Supón que la Solicitud A ejecuta exitosamente su transacción en PostgreSQL (COMMIT confirmado en t=9 ms). - En t=9.5 ms (antes de que el servidor envíe el paquete de respuesta 201 Created), el cliente sufre una desconexión de red y el servidor web cancela la tarea destruyendo el futuro (drop). - Responde: ¿Se revertirá la fila en PostgreSQL automáticamente por haberse cancelado el futuro de Rust? - Si el cliente reintenta la misma operación 5 segundos después con la misma clave de idempotencia, describe cómo la persistencia debe reconciliar el estado para evitar duplicados.
  3. Refactorización de sección crítica con mutex: - Escribe un fragmento de código donde un struct de configuración en memoria protegido por un mutex sea leído antes de hacer una llamada HTTP a un servicio externo y actualizado después de la llamada. - Demuestra formalmente que ningún MutexGuard permanece vivo durante la instrucción .await.

Registra tus predicciones y análisis antes de abrir la guía de soluciones de backend.


Preguntas de comprensión profunda y diagnóstico socrático#

Reflexiona sobre estas cinco preguntas de razonamiento técnico antes de continuar:

Pregunta 1: La anatomía del bucle cerrado de CPU#

Considera este handler asíncrono en Actix Web:

RUST
async fn fibonacci_handler() -> HttpResponse {
    let result = calculate_fibonacci(50); // Función recursiva pura que toma 10 segundos de CPU
    HttpResponse::Ok().json(result)
}
  • Si tu servidor está configurado con 4 worker threads de Tokio y 4 usuarios realizan solicitudes simultáneas a fibonacci_handler:
  • ¿Qué ocurre con la quinta solicitud entrante que solo pide un archivo estático o un health check GET /health trivial?
  • ¿Por qué el health check falla con timeout a pesar de que el código del health check no contiene errores y toma 0.1 milisegundos en responder normalmente?
  • ¿Cómo resuelve tokio::task::spawn_blocking este problema a nivel del planificador de tareas?

Pregunta 2: El costo en memoria del tamaño del enum del futuro#

En Rust, el tamaño de un enum es el tamaño de su variante más grande más la etiqueta de discriminante (discriminant):

RUST
async fn complex_flow() {
    let large_buffer = [0_u8; 1024 * 1024]; // 1 MB en el stack
    some_async_operation().await;
    println!("{}", large_buffer[0]);
}
  • ¿Cuánto espacio en memoria ocupa el futuro retornado por complex_flow() mientras está suspendido esperando a some_async_operation()?
  • Si spawneas 100,000 tareas concurrentes de esta función, ¿cuánta memoria RAM consumirá la aplicación y por qué mover buffers temporales masivos a un scope delimitado que se destruya antes del .await reduce drásticamente el consumo de memoria del backend?

Pregunta 3: El misterio de la cancelación asíncrona (Drop)#

Examina este bloque asíncrono que utiliza tokio::select!:

RUST
tokio::select! {
    res = send_money_transfer(&account_from, &account_to, 500) => {
        println!("Transfer confirmed: {res:?}");
    }
    _ = tokio::time::sleep(Duration::from_millis(100)) => {
        println!("Operation timed out");
    }
}
  • Si la llamada de red send_money_transfer toma 150 milisegundos en recibir el paquete de confirmación del banco remoto, ¿qué rama de select! se ejecuta?
  • ¿Qué le ocurre al futuro de send_money_transfer cuando la rama del timeout gana? ¿El dinero fue transferido en el banco remoto o no? ¿Por qué esta arquitectura es críticamente insegura si no se cuenta con claves de idempotencia transaccionales?

Pregunta 4: Por qué std::sync::MutexGuard no es Send#

  • A nivel del estándar POSIX de hilos (pthread_mutex_lock y pthread_mutex_unlock) y de las APIs del kernel de Windows: ¿Qué comportamiento indefinido (Undefined Behavior) o error ocurre si el Hilo de SO A bloquea un mutex y el Hilo de SO B intenta desbloquearlo?
  • ¿Cómo utiliza el sistema de tipos de Rust el trait marcador Send para hacer que esta violación sea físicamente imposible de compilar en programas multihilo?

Pregunta 5: Multitarea cooperativa vs recolección de basura (Garbage Collection)#

En lenguajes como Go o Java, los hilos virtuales o goroutines cuentan con puntos de interrupción automáticos inyectados por el compilador en los preámbulos de funciones (preemption checks).

  • ¿Qué ventajas ofrece el modelo de cero costo de Rust al no tener un recolector de basura (GC) ni puntos de interrupción forzados invisibles en tiempo de ejecución?
  • ¿Qué responsabilidad explícita asume el ingeniero de software en Rust respecto a la duración de los tramos de cómputo entre llamadas sucesivas a .await?

Code smell#

Síntoma: Retención de un mutex síncrono durante operaciones de E/S de red#

Encuentras el siguiente fragmento en un servicio de mensajería:

RUST
// Code smell crítico: mutex síncrono sostenido a través de .await
use std::sync::Mutex;

struct RoomManager {
    rooms: Mutex<HashMap<RoomId, RoomState>>,
}

impl RoomManager {
    async fn broadcast(&self, room_id: RoomId, message: Message) {
        let mut rooms_guard = self.rooms.lock().unwrap();
        let room = rooms_guard.get_mut(&room_id).unwrap();
        
        // ¡PELIGRO! Operación de red lenta reteniendo el mutex de toda la sala
        send_network_packet(&room.endpoint, &message).await;
        
        room.last_message_at = Instant::now();
    }
}
  • Evidencia observable: Un guardia de cerrojo síncrono (rooms_guard) se mantiene activo en la memoria mientras se ejecuta una operación de red asíncrona (.await).
  • Hipótesis del problema: El programador no advirtió que la espera de red puede demorar cientos de milisegundos o segundos si hay latencia.
  • Consecuencia concreta:
  1. Si se compila en un runtime multihilo, el compilador emitirá un error !Send impidiendo generar el binario.
  2. Si se burla el compilador o se usa un mutex asíncrono para retener el lock durante la red, ningún otro usuario podrá enviar mensajes ni consultar ninguna sala mientras dure la transmisión de ese paquete individual, destruyendo el rendimiento de la plataforma.
  • Tratamiento mínimo justificado:

Extraer la información necesaria bajo el lock rápidamente, soltar el lock, ejecutar la red asíncrona sin bloqueos, y re-adquirir el lock brevemente si se requiere actualizar el estado final:

RUST
  let endpoint = {
      let rooms_guard = self.rooms.lock().unwrap();
      rooms_guard.get(&room_id).unwrap().endpoint.clone()
  }; // El lock se libera inmediatamente

  send_network_packet(&endpoint, &message).await;

  {
      let mut rooms_guard = self.rooms.lock().unwrap();
      if let Some(room) = rooms_guard.get_mut(&room_id) {
          room.last_message_at = Instant::now();
      }
  }

Criterio de salida#

Puedes considerar superada esta lección con honores cuando:

  • Puedes explicar con tus propias palabras la diferencia fundamental entre multitarea cooperativa y apropiativa, y por qué un bucle de cómputo cerrado congela un worker thread de Rust.
  • Has ejecutado y verificado pruebas de futuros manuales con sondeo (poll) usando exclusivamente herramientas de la biblioteca estándar (rustc --test).
  • Puedes trazar paso a paso los dos fallos que provoca cruzar .await reteniendo un std::sync::MutexGuard (error de compilación !Send vs interbloqueo en runtime).
  • Entiendes con claridad que la cancelación de un futuro en Rust ocurre mediante la destrucción de su memoria (Drop) y que esto jamás cancela retroactivamente transacciones de base de datos o llamadas remotas ya emitidas.
  • Sabes cuándo delegar trabajo a tokio::task::spawn_blocking para proteger la capacidad de respuesta de los hilos de trabajo asíncronos.

Lecturas oficiales#


Siguiente paso#

¡Has completado con éxito el Módulo 02 de Backend! Continúa hacia la interfaz de usuario en 09 · HTML, CSS y diseño accesible, donde conectarás estos contratos HTTP y modelos de estado con el navegador web mediante semántica estándar y diseño accesible.

Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)