7.2 Общая полка
Самые опасные ошибки те, о которых компилятор молчит. Не потому что он их не видит -
он их попросту не проверяет: его область - типы и владение, не корректность поведения.
POST /tasks вернул задачу. GET /tasks вернул пустой список. Компилятор доволен.
Это не его ошибка - это ошибка выбора.
Что нужно
Первая задача решена: State передаёт хранилище обработчикам. Но каждый получает
свою копию вектора. Здесь - вторая задача: поделить между потоками, пока только для чтения.
Arc<T> - тип совместного владения: Arc::clone не копирует данные, он создаёт ещё
одного владельца тех же данных. Как именно это работает - в Lore в конце главы, если
интересно до кода.
У Arc есть граница: он позволяет вызывать методы, которые берут &self - list и get_task
только читают, им этого достаточно. add и done меняют данные - их пока оставим заглушками.
Постройка
Оборачиваем Vec<Task> в Arc. В list и get_task добавим eprintln! - он покажет
адрес вектора в памяти и докажет, что запросы смотрят на одно хранилище. add и done
тип тоже меняют, но остаются заглушками.
В crates/api/src/routes.rs:
// crates/api/src/routes.rs - CHANGED
use axum::{
Json, Router,
extract::{Path, State},
routing::{get, patch, post},
};
use std::sync::Arc;
use serde::Deserialize;
use tq_core::task::Task;
#[derive(Deserialize)]
struct AddRequest {
title: String,
}
// заглушка: задача не сохраняется - Arc не позволяет &mut
async fn add(_: State<Arc<Vec<Task>>>, Json(req): Json<AddRequest>) -> Json<Task> {
Json(Task::new(0, &req.title).unwrap())
}
// CHANGED: Arc<Vec<Task>>; eprintln! покажет адрес - он будет одинаковым для всех запросов
async fn list(State(tasks): State<Arc<Vec<Task>>>) -> Json<Vec<Task>> {
eprintln!("list: vec @ {:p}", tasks.as_ptr()); // {:p} - адрес вектора в памяти
Json(tasks.to_vec())
}
// CHANGED: Arc<Vec<Task>>; eprintln! для того же доказательства
async fn get_task(State(tasks): State<Arc<Vec<Task>>>, Path(id): Path<u64>) -> Json<Task> {
eprintln!("get: vec @ {:p}", tasks.as_ptr());
Json(tasks.iter().find(|t| t.id == id).unwrap().clone())
}
// заглушка: статус не меняется - Arc не позволяет &mut
async fn done(_: State<Arc<Vec<Task>>>, Path(id): Path<u64>) -> Json<Task> {
Json(Task::new(id, "stub").unwrap())
}
// CHANGED: router принимает Arc<Vec<Task>>
pub fn router(tasks: Arc<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)
}
В crates/api/src/main.rs - одно хранилище, обёрнутое в Arc:
// crates/api/src/main.rs - CHANGED
mod routes;
use axum::{Router, routing::get};
use std::sync::Arc;
use tq_core::task::Task;
async fn health() -> String {
"tq ok".to_string()
}
#[tokio::main]
async fn main() {
let tasks = Arc::new(vec![ // CHANGED: Arc<Vec<Task>> вместо Vec<Task>
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();
}
Результат
В одном терминале - сервер, в другом - два запроса подряд:
$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk",...},{"id":2,"title":"send report",...}]
$ curl -s localhost:3000/tasks/1
{"id":1,"title":"buy milk",...}
В терминале сервера:
list: vec @ 0x55f3a1b2c4d0
get: vec @ 0x55f3a1b2c4d0 ← тот же адрес
Здесь оба запроса работают с одним и тем же вектором.
add и done пока заглушены: как безопасно изменить общий вектор - следующий шаг.
Полный код
tqдля этой главы -7-many-hands/02-one-handle/.
Lore: Arc и счётчик ссылок
Arc расшифровывается как Atomically Reference Counted. Каждый Arc::clone атомарно
увеличивает счётчик; когда последний владелец уходит из области видимости, счётчик падает
до нуля и данные освобождаются. Атомарность означает, что несколько потоков могут
создавать и уничтожать владельцев одновременно. Цена - два атомарных инкремента на каждый
клон; у обычной ссылки этой цены нет. Для большинства приложений она не замечается.