Как создать глобальный, изменяемый singleton в Rust?
Правильный способ: Mutex + OnceCell или lazy_static
В Rust для создания глобального изменяемого singleton используется комбинация примитивов синхронизации и отложенной инициализации. Основная идея: мы оборачиваем данные в Mutex (или RwLock) для безопасного доступа из нескольких потоков, а сам объект инициализируем лениво через OnceCell или крейт lazy_static.
Вариант 1: Использование std::sync::LazyLock (Rust 1.80+)
Начиная с Rust 1.80, в стандартной библиотеке есть std::sync::LazyLock, который заменяет lazy_static и OnceCell:
use std::sync::{LazyLock, Mutex};
static CONFIG: LazyLock<Mutex<Vec<String>>> = LazyLock::new(|| {
Mutex::new(vec!["default".to_string()])
});
fn read_config() -> Vec<String> {
CONFIG.lock().unwrap().clone()
}
fn update_config(new: Vec<String>) {
let mut guard = CONFIG.lock().unwrap();
*guard = new;
}
Вариант 2: Классический lazy_static (для старых версий Rust)
Добавьте в Cargo.toml: lazy_static = "1.4"
use lazy_static::lazy_static;
use std::sync::Mutex;
lazy_static! {
static ref COUNTER: Mutex<i32> = Mutex::new(0);
}
fn increment() {
let mut num = COUNTER.lock().unwrap();
*num += 1;
}
Вариант 3: OnceCell + Mutex (без внешних крейтов)
Подходит для Rust 1.70+:
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
fn get_cache() -> &'static Mutex<HashMap<String, String>> {
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
Почему это безопасно?
- Mutex гарантирует, что в любой момент времени только один поток имеет доступ к данным. Если другой поток попытается захватить блокировку, он будет ждать.
- LazyLock/OnceLock обеспечивают однократную инициализацию при первом обращении — это потокобезопасно.
- Нет
unsafe: вся синхронизация проверяется компилятором.
Когда использовать RwLock вместо Mutex?
Если у вас много читателей и мало писателей, RwLock эффективнее:
static SETTINGS: LazyLock<RwLock<HashMap<String, String>>> = LazyLock::new(|| {
RwLock::new(HashMap::new())
});
fn read(key: &str) -> Option<String> {
let guard = SETTINGS.read().unwrap();
guard.get(key).cloned()
}
fn write(key: &str, value: String) {
let mut guard = SETTINGS.write().unwrap();
guard.insert(key.to_string(), value);
}
Важные замечания
- lock().unwrap() может вызвать панику, если другой поток запаниковал, удерживая блокировку. В production используйте обработку ошибок.
- Не держите блокировку долго — это снижает производительность.
- Избегайте deadlock: не захватывайте две блокировки одновременно в разных порядках.
Альтернативный подход: Atomic типы для простых значений
Если ваш singleton — это просто число или флаг, не используйте Mutex. В стандартной библиотеке есть атомарные типы, которые работают быстрее:
use std::sync::atomic::{AtomicI32, Ordering};
static COUNTER: AtomicI32 = AtomicI32::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::SeqCst);
}
fn get() -> i32 {
COUNTER.load(Ordering::SeqCst)
}
Атомарные операции не блокируют потоки и идеальны для счётчиков, флагов и простых состояний. Доступны: AtomicBool, AtomicI32, AtomicU64, AtomicPtr и другие.
Когда это не подходит
- Если данные сложнее одного числа (векторы, структуры) — нужен Mutex/RwLock.
- Если нужно читать и писать несколько полей как атомарную операцию — используйте Mutex.
Совет: для простых счётчиков всегда выбирайте атомарные типы — это быстрее и безопаснее, чем Mutex.
Ответов пока нет.
Войдите чтобы оставить ответ