Avatar ·

Как создать глобальный, изменяемый singleton в Rust?

Привет! Я изучаю Rust и пытаюсь сделать глобальную переменную, доступную из любой части программы, которую можно изменять. Например, мне нужно хранить конфигурацию приложения, которая загружается один раз при старте и потом может обновляться. В C++ или Python я бы просто сделал глобальную переменную, но в Rust компилятор ругается. Вот что я пробовал: ```rust static CONFIG: Vec = Vec::new(); // ошибка: не хватает времени жизни ``` Потом я попробовал `static mut`: ```rust static mut COUNTER: i32 = 0; fn increment() { unsafe { COUNTER += 1; } } ``` Это работает, но мне сказали, что `static mut` — это небезопасно и может привести к гонкам данных. Я слышал про `Mutex`, `OnceCell` и `lazy_static`, но не понимаю, как их правильно комбинировать. Мне нужно: - Инициализировать глобальную переменную один раз при старте программы - Читать её из нескольких потоков - Иногда изменять её (но не одновременно из разных потоков) Как правильно сделать такой singleton в Rust? Желательно без unsafe, если возможно. Дайте пример с пояснениями, пожалуйста. <<>>

Правильный способ: 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.