7.3 El cerrojo
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
El estante compartido resolvió la lectura. Añadir algo al estante es otra cuestión. Dos personas que intentan escribir en el mismo libro de cuentas al mismo tiempo no producen dos entradas sino una corrompida. Un taller que sabe esto mantiene la llave a la vista: la tomas - trabajas; no la tomas - esperas.
Lo que necesitas
De los tres problemas - pasar el almacén a los handlers, compartirlo entre hilos, modificarlo
con seguridad desde cualquiera de ellos - queda el tercero. Arc dio a todos el mismo
vector, pero modificarlo a través de Arc no está permitido.
Mutex<T> envuelve los datos y concede acceso a ellos de uno en uno: .lock() espera hasta
que otro titular lo libere, y devuelve un MutexGuard<T> - un guardián. Los métodos del
vector se llaman a través del guardián. Mientras el guardián está vivo, el cerrojo está
tomado; cuando sale del alcance, se libera automáticamente.
Arc<Mutex<Vec<Task>>> - un vector para todos, modificable en turnos. .lock() es siempre
necesario - tanto para lecturas como para escrituras: sin el cerrojo, los datos son
inaccesibles.
La construcción
Escribir Arc<Mutex<Vec<Task>>> en cada firma es trabajo innecesario. Un alias de tipo -
la misma técnica que TqResult<T> - lo resuelve de una vez. Al principio de
crates/api/src/routes.rs:
pub type SharedStore = Arc<Mutex<Vec<Task>>>;
.lock().unwrap() devuelve el guardián - un valor de tipo MutexGuard<Vec<Task>>. La
variable tasks a continuación es ese guardián: el vector es visible a través de él, y
mientras viva, el cerrojo está tomado.
El crates/api/src/routes.rs completo:
// crates/api/src/routes.rs - CAMBIADO
use axum::{
Json, Router,
extract::{Path, State},
routing::{get, patch, post},
};
use serde::Deserialize;
use std::sync::{Arc, Mutex, MutexGuard};
use tq_core::task::Task;
pub type SharedStore = Arc<Mutex<Vec<Task>>>;
#[derive(Deserialize)]
struct AddRequest {
title: String,
}
async fn add(State(store): State<SharedStore>, Json(req): Json<AddRequest>) -> Json<Task> {
let mut tasks: MutexGuard<Vec<Task>> = store.lock().unwrap(); // tasks es el guardián
let id = (tasks.len() + 1) as u64;
let task = Task::new(id, &req.title).unwrap();
tasks.push(task.clone());
Json(task)
}
async fn list(State(store): State<SharedStore>) -> Json<Vec<Task>> {
let tasks = store.lock().unwrap(); // sin mut - los métodos solo toman &self
Json(tasks.to_vec()) // Json requiere Vec<Task> con posesión; to_vec() copia desde el guardián
}
async fn get_task(State(store): State<SharedStore>, Path(id): Path<u64>) -> Json<Task> {
let tasks = store.lock().unwrap();
Json(tasks.iter().find(|t| t.id == id).unwrap().clone())
}
async fn done(State(store): State<SharedStore>, Path(id): Path<u64>) -> Json<Task> {
let mut tasks = store.lock().unwrap();
let task = tasks.iter_mut().find(|t| t.id == id).unwrap();
task.complete();
Json(task.clone())
}
pub fn router(store: SharedStore) -> Router {
Router::new()
.route("/tasks", post(add).get(list))
.route("/tasks/{id}", get(get_task))
.route("/tasks/{id}/done", patch(done))
.with_state(store)
}
En add, la task devuelta es una variable creada dentro de la función, independiente del
guardián. En list, se necesita el vector en sí: Json requiere un Vec<Task> con
posesión, mientras que el guardián solo cede &Vec<Task> - to_vec() hace la copia. En
todos los handlers el cerrojo se libera del mismo modo: automáticamente, cuando tasks sale
del alcance.
En crates/api/src/main.rs:
// crates/api/src/main.rs - CAMBIADO
mod routes;
use axum::{Router, routing::get};
use routes::SharedStore;
use std::sync::{Arc, Mutex};
async fn health() -> String {
"tq ok".to_string()
}
#[tokio::main]
async fn main() {
let store: SharedStore = Arc::new(Mutex::new(vec![]));
let app = Router::new()
.route("/", get(health))
.merge(routes::router(store));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
El resultado
El servidor en un terminal (make serve), cuatro peticiones en otro:
$ curl -s -X POST localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"buy milk"}'
{"id":1,"title":"buy milk","status":"Todo","created_at":"2026-06-20T08:47:13.382393472Z"}
$ curl -s -X POST localhost:3000/tasks -H "Content-Type: application/json" -d '{"title":"send report"}'
{"id":2,"title":"send report","status":"Todo","created_at":"2026-06-20T08:47:52.408233978Z"}
$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk","status":"Todo","created_at":"2026-06-20T08:47:13.382393472Z"},{"id":2,"title":"send report","status":"Todo","created_at":"2026-06-20T08:47:52.408233978Z"}]
$ curl -s -X PATCH localhost:3000/tasks/1/done
{"id":1,"title":"buy milk","status":"Done","created_at":"2026-06-20T08:47:13.382393472Z"}
Las tareas sobreviven entre peticiones. Reinicia el servidor y desaparecen: el almacén vive en la memoria del proceso. La persistencia entre reinicios es un problema aparte.
El código completo de
tqpara este capítulo está en7-many-hands/03-the-lock/.
Lore: Cuando cae el guardián
MutexGuard<T> mantiene el cerrojo mientras está vivo. Cuando sale del alcance, el cerrojo
se libera automáticamente. En los handlers anteriores, el guardián vive hasta el final de la
función: una petición retiene el almacén hasta que termina. Retener un cerrojo más tiempo
del necesario significa retrasar a todos los demás. En un servidor multihilo bajo carga
elevada eso importa - por ahora no hay que preocuparse por ello.