Avatar ·

Можно ли использовать глобальные переменные в Rust? Как объявить глобальный state?

Привет! Я пишу на Rust уже пару недель и столкнулся с проблемой. Мне нужно хранить конфигурацию приложения и счётчик запросов, доступные из разных модулей. В C++ я бы сделал так: ```rust // Моя попытка (не компилируется) static COUNTER: i32 = 0; static CONFIG: HashMap = HashMap::new(); fn increment() { COUNTER += 1; // Ошибка: cannot assign to immutable static } fn main() { CONFIG.insert("host".to_string(), "localhost".to_string()); // Ошибка println!("{}", COUNTER); } ``` Компилятор ругается на изменяемость и про `HashMap` говорит что-то про `const fn` и аллокации. Я читал, что в Rust глобальные переменные — это сложно, но как тогда делать общие настройки? Мне нужно: 1. Простой счётчик, который можно увеличивать из разных потоков. 2. Конфиг (HashMap), который заполняется при старте и потом только читается. 3. Логгер, который пишет в файл, и его нужно инициализировать один раз. Пробовал `lazy_static!` — не до конца понял. Помогите разобраться, как правильно работать с глобальными данными в Rust, чтобы не было гонок и unsafe кода. <<>>

Почему глобальные переменные — это сложно в 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 с расширениями. Глобалы оставьте для случаев, когда без них действительно не обойтись (логгеры, аллокаторы).