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


142

Как лучше всего создать и использовать структуру только с одним экземпляром в системе? Да, это необходимо, это подсистема OpenGL, и создание нескольких ее копий и их распространение повсюду добавят путаницы, а не уменьшат ее.

Синглтон должен быть максимально эффективным. Кажется невозможным хранить произвольный объект в статической области, поскольку он содержит Vecдеструктор. Второй вариант - сохранить (небезопасный) указатель в статической области, указывающий на выделенный синглтон в куче. Каков наиболее удобный и безопасный способ сделать это с кратким синтаксисом?


1
Вы смотрели, как существующие привязки Rust для OpenGL справляются с этой же проблемой?
Shepmaster 06

20
Да, это необходимо, это подсистема OpenGL, и создание нескольких ее копий и их распространение повсюду добавят путаницы, а не уменьшат ее. => это не определение необходимости , это может быть удобно (сначала), но не обязательно.
Matthieu M.

3
Да, вы правы. Хотя, поскольку OpenGL в любом случае представляет собой большой конечный автомат, я почти уверен, что нигде не будет его клона, использование которого приведет только к ошибкам OpenGL.
stevenkucera 07

Ответы:


200

Ответ без ответа

Избегайте глобального состояния в целом. Вместо этого создайте объект где-нибудь на раннем этапе (возможно, внутри main), а затем передайте изменяемые ссылки на этот объект в места, где он нужен. Обычно это облегчает понимание вашего кода и не требует большого изгиба назад.

Внимательно посмотрите на себя в зеркало, прежде чем решить, что вам нужны глобальные изменяемые переменные. В редких случаях это полезно, поэтому стоит знать, как это сделать.

Все еще хотите сделать ...?

Использование lazy-static

Ленивые статическая обрешетка может забрать некоторые из нудного создания синглтона вручную. Вот глобальный изменчивый вектор:

use lazy_static::lazy_static; // 1.4.0
use std::sync::Mutex;

lazy_static! {
    static ref ARRAY: Mutex<Vec<u8>> = Mutex::new(vec![]);
}

fn do_a_call() {
    ARRAY.lock().unwrap().push(1);
}

fn main() {
    do_a_call();
    do_a_call();
    do_a_call();

    println!("called {}", ARRAY.lock().unwrap().len());
}

Если вы удалите, Mutexто у вас будет глобальный синглтон без какой-либо изменчивости.

Вы также можете использовать RwLockвместо a, Mutexчтобы разрешить одновременное использование нескольких читателей.

Использование Once_cell

Once_cell обрешетка может забрать некоторые из нудного создания синглтона вручную. Вот глобальный изменчивый вектор:

use once_cell::sync::Lazy; // 1.3.1
use std::sync::Mutex;

static ARRAY: Lazy<Mutex<Vec<u8>>> = Lazy::new(|| Mutex::new(vec![]));

fn do_a_call() {
    ARRAY.lock().unwrap().push(1);
}

fn main() {
    do_a_call();
    do_a_call();
    do_a_call();

    println!("called {}", ARRAY.lock().unwrap().len());
}

Если вы удалите, Mutexто у вас будет глобальный синглтон без какой-либо изменчивости.

Вы также можете использовать RwLockвместо a, Mutexчтобы разрешить одновременное использование нескольких читателей.

Особый случай: атомика

Если вам нужно отслеживать только целочисленное значение, вы можете напрямую использовать атомарное :

use std::sync::atomic::{AtomicUsize, Ordering};

static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);

fn do_a_call() {
    CALL_COUNT.fetch_add(1, Ordering::SeqCst);
}

fn main() {
    do_a_call();
    do_a_call();
    do_a_call();

    println!("called {}", CALL_COUNT.load(Ordering::SeqCst));
}

Ручная реализация без зависимостей

Это сильно заимствовано из реализации Rust 1.0stdin с некоторыми настройками для современного Rust. Также стоит посмотреть на современную реализацию io::Lazy. Я прокомментировал, что делает каждая строка.

use std::sync::{Arc, Mutex, Once};
use std::time::Duration;
use std::{mem, thread};

#[derive(Clone)]
struct SingletonReader {
    // Since we will be used in many threads, we need to protect
    // concurrent access
    inner: Arc<Mutex<u8>>,
}

fn singleton() -> SingletonReader {
    // Initialize it to a null value
    static mut SINGLETON: *const SingletonReader = 0 as *const SingletonReader;
    static ONCE: Once = Once::new();

    unsafe {
        ONCE.call_once(|| {
            // Make it
            let singleton = SingletonReader {
                inner: Arc::new(Mutex::new(0)),
            };

            // Put it in the heap so it can outlive this call
            SINGLETON = mem::transmute(Box::new(singleton));
        });

        // Now we give out a copy of the data that is safe to use concurrently.
        (*SINGLETON).clone()
    }
}

fn main() {
    // Let's use the singleton in a few threads
    let threads: Vec<_> = (0..10)
        .map(|i| {
            thread::spawn(move || {
                thread::sleep(Duration::from_millis(i * 10));
                let s = singleton();
                let mut data = s.inner.lock().unwrap();
                *data = i as u8;
            })
        })
        .collect();

    // And let's check the singleton every so often
    for _ in 0u8..20 {
        thread::sleep(Duration::from_millis(5));

        let s = singleton();
        let data = s.inner.lock().unwrap();
        println!("It is: {}", *data);
    }

    for thread in threads.into_iter() {
        thread.join().unwrap();
    }
}

Это распечатывает:

It is: 0
It is: 1
It is: 1
It is: 2
It is: 2
It is: 3
It is: 3
It is: 4
It is: 4
It is: 5
It is: 5
It is: 6
It is: 6
It is: 7
It is: 7
It is: 8
It is: 8
It is: 9
It is: 9
It is: 9

Этот код компилируется с Rust 1.42.0. Реальные реализации Stdinиспользуют некоторые нестабильные функции, чтобы попытаться освободить выделенную память, чего не делает этот код.

На самом деле, вы, вероятно , хотите , чтобы SingletonReaderреализовать Derefи DerefMutпоэтому вы не должны совать в объект и зафиксировать его самостоятельно.

Вся эта работа - это то, что делают за вас lazy-static или once_cell.

Значение слова «глобальный»

Обратите внимание, что вы по-прежнему можете использовать обычную область видимости Rust и конфиденциальность на уровне модуля для управления доступом к переменной staticили lazy_static. Это означает, что вы можете объявить его в модуле или даже внутри функции, и он не будет доступен за пределами этого модуля / функции. Это хорошо для контроля доступа:

use lazy_static::lazy_static; // 1.2.0

fn only_here() {
    lazy_static! {
        static ref NAME: String = String::from("hello, world!");
    }

    println!("{}", &*NAME);
}

fn not_here() {
    println!("{}", &*NAME);
}
error[E0425]: cannot find value `NAME` in this scope
  --> src/lib.rs:12:22
   |
12 |     println!("{}", &*NAME);
   |                      ^^^^ not found in this scope

Однако переменная по-прежнему является глобальной, поскольку существует один ее экземпляр во всей программе.


72
После долгих размышлений я убежден, что не стоит использовать синглтон, а вместо этого вообще не использовать глобальные переменные и передавать все вокруг. Делает код более самодокументированным, поскольку ясно, какие функции обращаются к средству визуализации. Если я захочу вернуться к синглтону, это будет легче сделать, чем наоборот.
stevenkucera 08

4
Спасибо за ответ, очень помогло. Я просто подумал, что дам здесь комментарий, чтобы описать то, что я считаю допустимым вариантом использования lazy_static !. Я использую его для взаимодействия с приложением C, которое позволяет загружать / выгружать модули (общие объекты), и код ржавчины является одним из этих модулей. Я не вижу особых вариантов, кроме использования global on load, потому что у меня нет никакого контроля над main () и тем, как основное приложение взаимодействует с моим модулем. Мне в основном нужен вектор вещей, которые можно добавить во время выполнения после загрузки моего мода.
Мойзес Сильва,

1
@MoisesSilva всегда будет какая-то причина нуждаться в синглтоне, но во многих случаях его нет необходимости использовать. Не зная вашего кода, возможно, что приложение C должно разрешить каждому модулю возвращать «пользовательские данные», void *которые затем передаются обратно в методы каждого модуля. Это типичный шаблон расширения для кода C. Если приложение не позволяет это сделать и вы не можете его изменить, то да, синглтон может быть хорошим решением.
Shepmaster

3
@Worik, не могли бы вы объяснить почему? Я отговариваю людей делать то, что является плохой идеей для большинства языков (даже OP согласился, что глобальный вариант - плохой выбор для их приложения). Вот что в общем значит. Затем я показываю два решения, как это сделать в любом случае. Я только что протестировал lazy_staticпример в Rust 1.24.1, и он работает точно. Здесь external staticнигде нет . Возможно, вам нужно кое-что проверить, чтобы убедиться, что вы полностью поняли ответ.
Shepmaster

1
@Worik, если вам нужна помощь в основах работы с ящиком, я предлагаю вам перечитать язык программирования Rust . В главе о создании игры в угадайку показано, как добавлять зависимости.
Shepmaster

0

Используйте SpinLock для глобального доступа.

#[derive(Default)]
struct ThreadRegistry {
    pub enabled_for_new_threads: bool,
    threads: Option<HashMap<u32, *const Tls>>,
}

impl ThreadRegistry {
    fn threads(&mut self) -> &mut HashMap<u32, *const Tls> {
        self.threads.get_or_insert_with(HashMap::new)
    }
}

static THREAD_REGISTRY: SpinLock<ThreadRegistry> = SpinLock::new(Default::default());

fn func_1() {
    let thread_registry = THREAD_REGISTRY.lock();  // Immutable access
    if thread_registry.enabled_for_new_threads {
    }
}

fn func_2() {
    let mut thread_registry = THREAD_REGISTRY.lock();  // Mutable access
    thread_registry.threads().insert(
        // ...
    );
}

Если вам нужно изменяемое состояние (НЕ Singleton), см. Дополнительные описания в разделе « Чего не следует делать в Rust» .

Надеюсь, это поможет.


-1

Отвечая на мой собственный повторяющийся вопрос .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Корень ящика (lib.rs):

#[macro_use]
extern crate lazy_static;

Инициализация (небезопасный блок не нужен):

/// EMPTY_ATTACK_TABLE defines an empty attack table, useful for initializing attack tables
pub const EMPTY_ATTACK_TABLE: AttackTable = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

lazy_static! {
    /// KNIGHT_ATTACK is the attack table of knight
    pub static ref KNIGHT_ATTACK: AttackTable = {
        let mut at = EMPTY_ATTACK_TABLE;
        for sq in 0..BOARD_AREA{
            at[sq] = jump_attack(sq, &KNIGHT_DELTAS, 0);
        }
        at
    };
    ...

РЕДАКТИРОВАТЬ:

Удалось решить с помощью once_cell, для которого не нужен макрос.

Cargo.toml:

[dependencies]
once_cell = "1.3.1"

square.rs:

use once_cell::sync::Lazy;

...

/// AttackTable type records an attack bitboard for every square of a chess board
pub type AttackTable = [Bitboard; BOARD_AREA];

/// EMPTY_ATTACK_TABLE defines an empty attack table, useful for initializing attack tables
pub const EMPTY_ATTACK_TABLE: AttackTable = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

/// KNIGHT_ATTACK is the attack table of knight
pub static KNIGHT_ATTACK: Lazy<AttackTable> = Lazy::new(|| {
    let mut at = EMPTY_ATTACK_TABLE;
    for sq in 0..BOARD_AREA {
        at[sq] = jump_attack(sq, &KNIGHT_DELTAS, 0);
    }
    at
});

2
Этот ответ не дает ничего нового по сравнению с существующими ответами, которые уже обсуждаются lazy_staticи более новыми once_cell. Смысл маркировки вещей как дубликатов на SO - избежать избыточной информации.
Shepmaster
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.