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#
- Completa las lecciones previas sobre ownership, préstamos,
Result, Actix Web y transacciones relacionales. - 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 traitFuture, el métodopolly la estructuraWaker, totalmente ejecutable y verificable conrustc --test. - Patrones asíncronos con Runtimes (Tokio / Actix): Fragmentos respaldados por dependencias externas para servidores en producción, etiquetados como crate-backed. - 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
asyncproporciona 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
asynchace 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,asyncsolo 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: Firmafn 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). RetornaPoll::Ready(valor)si concluyó, oPoll::Pendingsi aún espera un evento externo. Waker: Puntero a una función callback dentro deContext. Cuando el evento físico de red o disco ocurre, el sistema operativo o reactor invocawaker.wake(), notificando al executor que esa tarea específica está lista para ser sondeada nuevamente conpoll.- Ejemplo en Rust estándar:
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
Promisesde JavaScript. En JavaScript, unaPromisecomienza 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.awaito 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 (
epollen Linux,kqueueen macOS/BSD,IOCPen 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étodopoll. - 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ónasync fnen una máquina de estados representada internamente por unenumanónimo. Cada punto donde aparece la palabra clave.awaitse convierte en una variante del enum que almacena todas las variables locales vivas en ese instante. - Ejemplo visual de desazucarado sintáctico:
// 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::spawnasigna 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::Pendingen 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:
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 tipoTesSend, mover una variable de tipoTa 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:TesSyncsi y solo si&TesSend.- Ejemplo en tipos primitivos:
Rc<T>no esSendniSyncporque su contador de referencias interno no es atómico.Arc<T>sí esSendySync(siTesSend + Sync) porque utiliza operaciones atómicas de hardware (atomic fetch-and-add).- Concepción errónea: Creer que debes implementar
SendySyncmanualmente conunsafe. El compilador de Rust los deduce automáticamente para tus structs si todos sus campos miembros implementanSendySync.
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 llamadasfutexen Linux oSRWLocken 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 exigeSend(porqueMutexGuardno esSend).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 retornandoPoll::Pendingy cede el worker thread a otras tareas. Su guardiaMutexGuardsí implementaSend.- Regla de oro de decisión:
- Usa
std::sync::Mutexpara 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.awaitde 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
asyncrevierte 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:
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:
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:
- Las variables locales vivas que deben sobrevivir entre llamadas a
.await(comoauth) se convierten en campos de las variantes del enum. - Cada invocación a
.awaitgenera una rama en la funciónpoll(Pin<&mut Self>, &mut Context). - 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:
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."]
- 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
.awaity 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 astd::sync::MutexGuardcomo!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 implementaSendy emitirá el errorerror[E0277]:MutexGuardcannot be sent between threads safely. - 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#
// 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#
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]
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:
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:
// 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:#
- 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.
- *Escenario de desconexión y cancelación (Drop):*
- Supón que la Solicitud A ejecuta exitosamente su transacción en PostgreSQL (
COMMITconfirmado en t=9 ms). - En t=9.5 ms (antes de que el servidor envíe el paquete de respuesta201 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. - 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
MutexGuardpermanece 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:
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 /healthtrivial? - ¿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_blockingeste 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):
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 asome_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
.awaitreduce 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!:
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_transfertoma 150 milisegundos en recibir el paquete de confirmación del banco remoto, ¿qué rama deselect!se ejecuta? - ¿Qué le ocurre al futuro de
send_money_transfercuando 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_lockypthread_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
Sendpara 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:
// 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:
- Si se compila en un runtime multihilo, el compilador emitirá un error
!Sendimpidiendo generar el binario. - 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:
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
.awaitreteniendo unstd::sync::MutexGuard(error de compilación!Sendvs 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_blockingpara proteger la capacidad de respuesta de los hilos de trabajo asíncronos.
Lecturas oficiales#
- Rust Book: Asynchronous Programming (Capítulo 17): Futuros, sintaxis
async/awaity modelos de concurrencia del lenguaje. - The Rust Async Book: Guía canónica sobre la mecánica interna de
Future,Wakery diseño de ejecutores. - Tokio: Tutorial y Guía Oficial: Arquitectura de tareas, canales,
spawn_blockingy primitivas de sincronización. - Tokio: Cancel Safety: Reglas formales de seguridad ante cancelación en operaciones asíncronas.
- Glosario técnico del curso: Definiciones de concurrencia y runtime.
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.