Можно ли использовать глобальные переменные в Rust? Как объявить глобальный state?
Почему глобальные переменные — это сложно в Rust?
Rust стремится к безопасности на уровне компиляции. Обычные глобальные переменные (static) имеют строгие ограничения:
- Неизменяемость по умолчанию —
static COUNTER: i32 = 0;нельзя менять, так как это привело бы к гонкам данных в многопоточной среде. - Инициализация только константами —
HashMap::new()не является константной функцией, поэтому её нельзя использовать напрямую. - Требуется
Sync— тип глобальной переменной должен быть потокобезопасным.
Решение 1: std::sync::Mutex + once_cell (современный подход)
Лучший способ — использовать крейт once_cell (или std::sync::LazyLock в Rust 1.80+). Это позволяет лениво инициализировать глобальные данные с безопасным доступом.
// Cargo.toml
// [dependencies]
// once_cell = "1.19"
// serde = { version = "1.0", features = ["derive"] }
use once_cell::sync::Lazy;
use std::sync::Mutex;
use std::collections::HashMap;
// 1. Счётчик с Mutex (потокобезопасный)
static COUNTER: Lazy<Mutex<i32>> = Lazy::new(|| Mutex::new(0));
fn increment_counter() {
let mut count = COUNTER.lock().unwrap();
*count += 1;
}
// 2. Конфиг — заполняется один раз, потом только читается
static CONFIG: Lazy<Mutex<HashMap<String, String>>> = Lazy::new(|| {
let mut map = HashMap::new();
map.insert("host".to_string(), "localhost".to_string());
map.insert("port".to_string(), "8080".to_string());
Mutex::new(map)
});
fn get_config(key: &str) -> Option<String> {
CONFIG.lock().unwrap().get(key).cloned()
}
fn main() {
increment_counter();
increment_counter();
println!("Count: {}", *COUNTER.lock().unwrap()); // 2
println!("Host: {}", get_config("host").unwrap());
}
Решение 2: std::sync::RwLock для частого чтения
Если вы часто читаете конфиг и редко пишете, используйте RwLock — он позволяет множеству потоков читать одновременно:
use std::sync::RwLock;
static CONFIG_RW: Lazy<RwLock<HashMap<String, String>>> = Lazy::new(|| {
let map = /* ... */;
RwLock::new(map)
});
// Чтение (много потоков могут читать одновременно)
fn get_host() -> String {
CONFIG_RW.read().unwrap()["host"].clone()
}
// Запись (блокирует всех читателей)
fn update_config(key: &str, value: String) {
CONFIG_RW.write().unwrap().insert(key.to_string(), value);
}
Решение 3: static с Atomic для простых чисел
Для счётчиков и флагов используйте атомарные типы — они не требуют блокировок:
use std::sync::atomic::{AtomicI32, Ordering};
static COUNTER_ATOMIC: AtomicI32 = AtomicI32::new(0);
fn increment_atomic() {
COUNTER_ATOMIC.fetch_add(1, Ordering::SeqCst);
}
fn main() {
increment_atomic();
println!("{}", COUNTER_ATOMIC.load(Ordering::SeqCst)); // 1
}
Решение 4: Крейт lazy_static (устаревший, но популярный)
Если вы видели lazy_static!, вот как это работает:
use lazy_static::lazy_static;
use std::sync::Mutex;
lazy_static! {
static ref COUNTER: Mutex<i32> = Mutex::new(0);
static ref CONFIG: Mutex<HashMap<String, String>> = {
let mut m = HashMap::new();
m.insert("key".into(), "value".into());
Mutex::new(m)
};
}
Недостаток: lazy_static использует макросы и может быть менее прозрачным. Рекомендую once_cell или std::sync::LazyLock.
Важные правила
- Всегда обрабатывайте ошибки
lock().unwrap()— в реальном коде используйте.expect("poisoned lock")или обработку. - Не храните большие объёмы данных в глобалах — это усложняет тестирование.
- Рассмотрите Dependency Injection (например, через
Arc<Config>) вместо глобалов.
Альтернативный подход: Инверсия зависимостей
Глобальные переменные — это антипаттерн, который усложняет тестирование и параллелизм. Вместо этого передавайте состояние через структуры:
struct AppState {
counter: AtomicI32,
config: HashMap<String, String>,
}
fn main() {
let state = Arc::new(AppState {
counter: AtomicI32::new(0),
config: HashMap::from([("host".into(), "localhost".into())]),
});
// Передаём Arc в потоки
let state_clone = state.clone();
std::thread::spawn(move || {
state_clone.counter.fetch_add(1, Ordering::SeqCst);
});
// Или используем мьютекс для изменяемых данных
let state_mutex = Arc::new(Mutex::new(AppState { /* ... */ }));
}
Плюсы: легко тестировать (подменяем состояние), нет глобалов, всё явно. Минусы: нужно передавать состояние через все функции. Для больших проектов используйте крейты типа actix-web с Data<AppState> или axum с расширениями. Глобалы оставьте для случаев, когда без них действительно не обойтись (логгеры, аллокаторы).
Ответов пока нет.
Войдите чтобы оставить ответ