7.3 Блокировка
Общая полка решила задачу чтения. Добавить что-то на полку - другая история. Двое, пытающихся вписать в один журнал одновременно, создают не две записи, а одну испорченную. Мастерская, которая знает об этом, держит ключ на виду: взял - работаешь, не взял - ждёшь.
Что нужно
Из трёх задач - передать хранилище обработчикам, поделить между потоками, безопасно менять
из любого из них - осталась третья. Arc дал всем один вектор, но изменить его через
Arc нельзя.
Mutex<T> оборачивает данные и даёт к ним доступ по одному: .lock() ждёт, пока другой
не отпустит, и возвращает MutexGuard<T> - охранника. Через охранника вызываются методы
вектора. Пока охранник жив - блокировка удерживается; когда уходит из области видимости -
отпускается автоматически.
Arc<Mutex<Vec<Task>>> - один вектор на всех, изменяемый по очереди. lock() нужен
всегда - и для чтения, и для записи: без блокировки к данным не дотянуться.
Постройка
Писать Arc<Mutex<Vec<Task>>> в каждой сигнатуре - лишняя работа. Псевдоним типа -
тот же приём, что TqResult<T> - решает это один раз. В начале
crates/api/src/routes.rs:
pub type SharedStore = Arc<Mutex<Vec<Task>>>;
.lock().unwrap() возвращает охранника - значение типа MutexGuard<Vec<Task>>. Переменная
tasks ниже - это и есть охранник: через неё виден вектор, и пока она жива, блокировка
удерживается.
Полный crates/api/src/routes.rs:
// crates/api/src/routes.rs - CHANGED
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 - охранник
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(); // без mut - методы только &self
Json(tasks.to_vec()) // Json требует owned Vec<Task>; to_vec() копирует из охранника
}
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)
}
В add возвращается task - переменная, созданная внутри функции и с охранником не
связанная. В list нужен сам вектор: Json требует owned Vec<Task>, а охранник даёт
только &Vec<Task> - to_vec() делает копию. Во всех обработчиках блокировка снимается
одинаково: автоматически, когда tasks покидает область видимости.
В crates/api/src/main.rs:
// crates/api/src/main.rs - CHANGED
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();
}
Результат
В одном терминале - сервер (make serve), в другом - четыре запроса:
$ 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"}
Задачи живут между запросами. Сервер перезапустится - они исчезнут: хранилище в памяти процесса. Сохранить между запусками - отдельная задача.
Полный код
tqдля этой главы -7-many-hands/03-the-lock/.
Lore: когда охранник уходит
MutexGuard<T> удерживает блокировку, пока жив. Когда уходит из области видимости -
блокировка отпускается автоматически. В обработчиках выше охранник живёт до конца
функции: один запрос держит хранилище пока не закончит работу. Держать блокировку
дольше, чем нужно, значит задерживать всех остальных. В многопоточном сервере с высокой
нагрузкой это важно - сейчас можно не беспокоиться.