Какова роль Arc при разделении состояния между обработчиками Axum?
Arc позволяет разделить владение данными состояния между несколькими обработчиками без копирования. Axum клонирует State при каждом запросе, поэтому тип состояния должен реализовывать Clone — Arc обеспечивает это дёшево.
Роль Arc при разделении состояния в Axum
Axum клонирует объект состояния при каждом входящем запросе и передаёт клон в обработчик через экстрактор State<T>. Это означает, что тип T обязан реализовывать трейт Clone. Для дешёвого клонирования без копирования самих данных используется Arc<T> — умный указатель с подсчётом ссылок, атомарный и потокобезопасный.
Базовый пример с Arc
use axum::{
extract::State,
routing::get,
Router, Json,
};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
// Состояние приложения — не Clone само по себе
struct AppState {
db: sqlx::PgPool,
cache: RwLock<HashMap<String, String>>,
config: AppConfig,
}
struct AppConfig {
max_items: usize,
}
async fn get_cached(
State(state): State<Arc<AppState>>,
) -> Json<Vec<String>> {
let cache = state.cache.read().await;
let values: Vec<String> = cache.values().cloned().collect();
Json(values)
}
async fn set_cached(
State(state): State<Arc<AppState>>,
) -> &'static str {
let mut cache = state.cache.write().await;
cache.insert("key".to_string(), "value".to_string());
"ok"
}
#[tokio::main]
async fn main() {
let state = Arc::new(AppState {
db: sqlx::PgPool::connect(&std::env::var("DATABASE_URL").unwrap())
.await
.unwrap(),
cache: RwLock::new(HashMap::new()),
config: AppConfig { max_items: 100 },
});
let app = Router::new()
.route("/cache", get(get_cached).post(set_cached))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Arc vs прямой Clone
Если состояние состоит только из дешёво клонируемых типов (например, только PgPool, который сам является обёрткой над Arc), то обёртка в Arc не нужна — .with_state(pool) достаточно. Arc нужен тогда, когда структура состояния не реализует Clone или её клонирование дорого.
Arc с мутабельным состоянием
Сам Arc даёт только разделённое чтение. Для мутации используют:
tokio::sync::Mutex— эксклюзивный доступ, приостанавливает task при блокировкеtokio::sync::RwLock— несколько читателей или один писательstd::sync::RwLock— блокирует поток, не подходит для async-кодаDashMap— конкурентная хэш-карта без явных блокировок
Подводные камни
- Использование
std::sync::Mutexвнутри Arc при async-коде вызывает блокировку потока Tokio и может привести к deadlock при удержании guard через await. - Слишком широкое состояние в одной структуре создаёт contention на RwLock — лучше разбить на несколько независимых Arc.
- Arc::clone(&state) стоит дёшево (атомарный инкремент счётчика), но это не Zero Cost — в горячих путях с тысячами запросов в секунду это заметно.
- Забытое Arc приводит к ошибке "the trait Clone is not implemented" — компилятор укажет на это, но сообщение может быть неочевидным.
- Хранение Arc<dyn Trait> требует явного
+ Send + Syncна трейте, иначе Axum откажет в компиляции. - Циклические ссылки через Arc приводят к утечкам памяти — для самореференций используют Weak<T>.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.