Как создать глобальный, изменяемый синглтон в Rust?
Правильный подход: `Mutex` + `OnceCell` или `lazy_static`
В Rust глобальное изменяемое состояние — это антипаттерн, но если без него не обойтись, нужно использовать потокобезопасные примитивы. Лучшее решение — комбинация OnceCell (или lazy_static) для ленивой инициализации и Mutex для безопасного доступа из нескольких потоков.
Решение с std::sync::OnceLock (Rust 1.70+)
Начиная с Rust 1.70, в стандартной библиотеке есть OnceLock, который заменяет lazy_static. Вот полный пример:
use std::sync::{Mutex, OnceLock};
struct Config {
logging: bool,
log_file: String,
}
impl Config {
fn new() -> Self {
Config {
logging: false,
log_file: String::from("app.log"),
}
}
}
fn get_config() -> &'static Mutex<Config> {
static CONFIG: OnceLock<Mutex<Config>> = OnceLock::new();
CONFIG.get_or_init(|| Mutex::new(Config::new()))
}
fn main() {
// Чтение
let config = get_config().lock().unwrap();
println!("Logging: {}", config.logging);
// Изменение
let mut config = get_config().lock().unwrap();
config.logging = true;
config.log_file = String::from("debug.log");
}
Решение с крейтом lazy_static
Если вы используете старую версию Rust или предпочитаете более лаконичный синтаксис, добавьте в Cargo.toml:
[dependencies]
lazy_static = "1.4"
И код:
use std::sync::Mutex;
use lazy_static::lazy_static;
struct Config {
logging: bool,
}
lazy_static! {
static ref CONFIG: Mutex<Config> = Mutex::new(Config { logging: false });
}
fn main() {
// Чтение
let config = CONFIG.lock().unwrap();
println!("Logging: {}", config.logging);
// Изменение
let mut config = CONFIG.lock().unwrap();
config.logging = true;
}
Важные моменты
- Mutex гарантирует, что только один поток может изменять данные в любой момент времени.
- OnceLock/lazy_static обеспечивают ленивую инициализацию — синглтон создаётся при первом обращении.
- Всегда используйте
unwrap()или обрабатывайте ошибкуPoisonError(если другой поток запаниковал, удерживая блокировку). - Не держите блокировку дольше, чем необходимо — область видимости
lock()ограничена блоком или функцией.
Альтернатива: RwLock для частого чтения
Если ваша конфигурация читается гораздо чаще, чем изменяется, используйте RwLock вместо Mutex. Он позволяет множеству потоков читать одновременно:
use std::sync::{RwLock, OnceLock};
fn get_config() -> &'static RwLock<Config> {
static CONFIG: OnceLock<RwLock<Config>> = OnceLock::new();
CONFIG.get_or_init(|| RwLock::new(Config::new()))
}
// Чтение
let config = get_config().read().unwrap();
// Запись
let mut config = get_config().write().unwrap();
Помните: глобальное состояние усложняет тестирование и делает код менее предсказуемым. По возможности передавайте конфигурацию через параметры функций или используйте внедрение зависимостей.
<<Решение с атомарными операциями (для простых типов)
Если ваш синглтон содержит только простые типы (числа, булевы значения), можно обойтись без Mutex с помощью атомарных типов из std::sync::atomic. Это быстрее, но подходит только для примитивов.
use std::sync::atomic::{AtomicBool, Ordering};
struct Config {
logging: AtomicBool,
}
impl Config {
fn new() -> Self {
Config { logging: AtomicBool::new(false) }
}
}
fn get_config() -> &'static Config {
static CONFIG: Config = Config::new();
&CONFIG
}
fn main() {
// Установка
get_config().logging.store(true, Ordering::SeqCst);
// Чтение
let is_logging = get_config().logging.load(Ordering::SeqCst);
println!("Logging: {}", is_logging);
}
Для более сложных структур (строки, векторы) атомарные типы не подходят — используйте Mutex или RwLock. Также можно рассмотреть AtomicPtr для указателей, но это сложнее и требует осторожности.
Ещё один вариант — крейт once_cell (предшественник OnceLock), который работает на старых версиях Rust:
use once_cell::sync::OnceCell;
use std::sync::Mutex;
static CONFIG: OnceCell<Mutex<Config>> = OnceCell::new();
fn get_config() -> &'static Mutex<Config> {
CONFIG.get_or_init(|| Mutex::new(Config::new()))
}