← Оглавление

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> удерживает блокировку, пока жив. Когда уходит из области видимости - блокировка отпускается автоматически. В обработчиках выше охранник живёт до конца функции: один запрос держит хранилище пока не закончит работу. Держать блокировку дольше, чем нужно, значит задерживать всех остальных. В многопоточном сервере с высокой нагрузкой это важно - сейчас можно не беспокоиться.