7.1 El estado que comparten dos hilos
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
En un taller con varios artesanos, un único libro de cuentas reposa en un estante compartido. No en el banco del primero, ni en el del segundo - en el estante, donde todos tienen acceso. De lo contrario, cada uno lleva su propio registro, y al final del día nadie conoce el cuadro completo. Cuando no hay estante compartido, cada uno tiene su propia versión. Y que cada uno tenga su propia versión significa que nadie tiene la verdad.
Lo que necesitas
Para conectar el almacén al servidor hay que resolver tres problemas: cómo pasárselo a los handlers, cómo compartirlo entre varios hilos y cómo modificarlo con seguridad desde cualquiera de ellos. Cada uno es un capítulo aparte. Este es el primero.
Los handlers son funciones que axum llama una vez por cada petición entrante. add, list,
get_task no saben nada de main() y no tienen acceso a sus variables. Se necesita un
almacén que sobreviva a una sola petición y sea accesible a todos los handlers a la vez.
Antes de conectar TaskStore, el mecanismo en sí se demostrará con Vec<Task> haciendo
las veces de almacén.
axum proporciona State para esto: .with_state() adjunta un objeto al router, y el
extractor State<S> - el mismo principio que Path y Json - lo recupera en un handler.
State<S> exige que S implemente Clone: axum clona el estado en cada llamada al
handler.
La construcción
Vec<T> implementa Clone solo cuando T: Clone - así que Clone es necesario en
Task, y también en Status, ya que es un campo de Task. En
crates/core/src/task/mod.rs:
// CAMBIADO: Clone es necesario para que Vec<Task> pueda implementar Clone
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Status { Todo, Done }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Task { ... }
En crates/api/src/routes.rs, reemplaza los stubs con State<Vec<Task>>. Añade
eprintln! a add y list - imprimirá la dirección del vector en memoria y hará visible
lo que ocurre con los datos en cada petición:
// crates/api/src/routes.rs - CAMBIADO
use axum::{
Json, Router,
extract::{Path, State},
routing::{get, patch, post},
};
use serde::Deserialize;
use tq_core::task::Task;
#[derive(Deserialize)]
struct AddRequest {
title: String,
}
// State(mut tasks) - axum pasa la propiedad del clon; mut es necesario para modificar el vec
async fn add(State(mut tasks): State<Vec<Task>>, Json(req): Json<AddRequest>) -> Json<Task> {
eprintln!("add: vec @ {:p}", tasks.as_ptr()); // {:p} - dirección del vector en memoria
let id = (tasks.len() + 1) as u64;
let task = Task::new(id, &req.title).unwrap();
tasks.push(task.clone()); // clon: la tarea se necesita tanto en el vec como en la respuesta
Json(task)
}
async fn list(State(tasks): State<Vec<Task>>) -> Json<Vec<Task>> {
eprintln!("list: vec @ {:p}", tasks.as_ptr());
Json(tasks)
}
async fn get_task(State(tasks): State<Vec<Task>>, Path(id): Path<u64>) -> Json<Task> {
Json(tasks.into_iter().find(|t| t.id == id).unwrap())
}
async fn done(State(mut tasks): State<Vec<Task>>, Path(id): Path<u64>) -> Json<Task> {
let task = tasks.iter_mut().find(|t| t.id == id).unwrap();
task.complete();
Json(task.clone())
}
pub fn router(tasks: Vec<Task>) -> Router {
Router::new()
.route("/tasks", post(add).get(list))
.route("/tasks/{id}", get(get_task))
.route("/tasks/{id}/done", patch(done))
.with_state(tasks) // adjunta el estado al router
}
En crates/api/src/main.rs, ensambla el estado inicial y pásalo al router:
// crates/api/src/main.rs - CAMBIADO
mod routes;
use axum::{Router, routing::get};
use tq_core::task::Task;
async fn health() -> String {
"tq ok".to_string()
}
#[tokio::main]
async fn main() {
let tasks: Vec<Task> = vec![ // estado inicial: axum lo clona en cada petición
Task::new(1, "buy milk").unwrap(),
Task::new(2, "send report").unwrap(),
];
let app = Router::new()
.route("/", get(health))
.merge(routes::router(tasks));
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), tres peticiones en otro:
$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk",...},{"id":2,"title":"send report",...}]
$ curl -s -X POST localhost:3000/tasks \
-H "Content-Type: application/json" -d '{"title":"call dentist"}'
{"id":3,"title":"call dentist",...}
$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk",...},{"id":2,"title":"send report",...}]
call dentist ha desaparecido. El servidor aceptó la petición, devolvió la tarea con id 3
- pero el siguiente
listno la ve. El terminal del servidor muestra por qué:
list: vec @ 0x55f3a1b2c4d0
add: vec @ 0x55f3a1b2c5e8 ← dirección distinta
list: vec @ 0x55f3a1b2c6a0 ← distinta otra vez
Cada petición recibió su propio clon del vector. add añadió la tarea a su clon - y se la
llevó cuando la petición terminó. El siguiente list no sabía nada de call dentist.
Clone satisfizo al compilador. Cada handler tiene ahora un libro de cuentas - solo que el
suyo propio. El libro en el estante compartido es el siguiente paso.
El código completo de
tqpara este capítulo está en7-many-hands/01-the-state-two-threads-share/.