Avatar ·

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

Привет, коллеги! Я новичок в Rust и пытаюсь реализовать глобальную конфигурацию для своего приложения. Мне нужно, чтобы из любого места кода можно было прочитать и изменить настройки (например, уровень логирования или путь к файлу). В других языках я бы просто сделал статическую переменную, но в Rust всё сложнее. Вот что я пробовал: ```rust static CONFIG: Config = Config { logging: false }; struct Config { logging: bool, } fn main() { CONFIG.logging = true; // Ошибка: cannot assign to static immutable } ``` Компилятор ругается, что статика неизменяема. Я попробовал `static mut`, но он требует `unsafe`: ```rust static mut CONFIG: Config = Config { logging: false }; fn main() { unsafe { CONFIG.logging = true; // Работает, но unsafe } } ``` Но использовать `unsafe` повсюду кажется неправильным. Плюс, я слышал о проблемах с потоками — моё приложение многопоточное. Есть ли безопасный способ сделать глобальный синглтон, который можно изменять? Я смотрел на `lazy_static` и `OnceCell`, но не уверен, что они решают проблему изменяемости. Подскажите, как правильно организовать глобальное состояние в 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()))
}