Як створити глобальний мутаційний сингл?


140

Який найкращий спосіб створити та використовувати структуру з однією інстанцією в системі? Так, це необхідно, це саме підсистема OpenGL, і створення декількох копій цього і передача його скрізь додасть плутанину, а не полегшить її.

Сінглтон повинен бути максимально ефективним. Зберігати довільний об'єкт на статичній ділянці не представляється можливим, оскільки він містить Vecдеструктор. Другий варіант - зберігати (небезпечний) вказівник на статичній ділянці, вказуючи на кучу, виділену синглтон. Який найзручніший і найбезпечніший спосіб зробити це, зберігаючи синтаксис стислим.


1
Ви подивилися, як існуючі прив'язки Rust для OpenGL вирішують цю проблему?
Шепмайстер

20
Так, це необхідно, це саме підсистема OpenGL, і створення декількох копій цього і передача його скрізь додасть плутанину, а не полегшить її. => це не визначення необхідного , це може бути зручно (спочатку), але не потрібно.
Матьє М.

3
Так, у вас є пункт. Хоча, оскільки OpenGL все-таки є великою державною машиною, я майже впевнений, що ніде не буде його клону, використання якого призведе лише до помилок OpenGL.
stevenkucera

Відповіді:


198

Відповідь без відповіді

Уникайте глобальної держави взагалі. Натомість побудуйте об’єкт десь рано (можливо, в main), а потім передайте змінні посилання на цей об’єкт у місця, які потребують цього. Це, як правило, полегшить міркування про ваш код і не потребує стільки згинання назад.

Подивіться важко на себе в дзеркало, перш ніж вирішити, що ви хочете глобальних змінних змінних. Є рідкісні випадки, коли це корисно, тому саме тому варто знати, як це робити.

Ще хочете зробити одне ...?

Використання ліниво-статичних

Ледачі статична лати може забрати деякі з нудного створення Сінглтона вручну. Ось глобальний змінний вектор:

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замість, 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замість, 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тому вам не доведеться засуватися в об’єкт і замикати його самостійно.

Вся ця робота - це те, що для вас роблять ліниві статичні або Once_cell.

Значення "глобальний"

Зверніть увагу, що ви все ще можете використовувати звичайне визначення рівня іржі та конфіденційність на рівні модуля, щоб контролювати доступ до змінної 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
Після багато роздумів я переконаний, що не використовувати Singleton, а замість цього взагалі не використовувати глобальних змінних і передавати все навколо. Робить код більш самодокументованим, оскільки зрозуміло, які функції мають доступ до візуалізації. Якщо я захочу повернутися на одиночний, зробити це буде простіше, ніж навпаки.
stevenkucera

4
Дякую за відповідь, це дуже допомогло. Я просто подумав, що дам тут коментар, щоб описати те, що я вважаю дійсним випадком використання для lazy_static !. Я використовую його для інтерфейсу з додатком C, який дозволяє завантажувати / вивантажувати модулі (спільні об'єкти), а код іржі є одним з цих модулів. Я не бачу багато варіантів, ніж використання глобального навантаження, оскільки я взагалі не контролюю main () і те, як основна програма взаємодіє з моїм модулем. Мені в основному був потрібен вектор речей, які можна додати під час виконання після завантаження мода.
Мойсей Сільва

1
@MoisesSilva завжди буде якась причина, коли потрібен синглтон, але це зайве використовувати його у багатьох випадках, коли він використовується. Не знаючи вашого коду, можливо, що програма C повинна дозволяти кожному модулю повертати "дані користувача", void *які потім передаються назад у методи кожного модуля. Це типовий шаблон розширення для коду С. Якщо програма не дозволяє цього, і ви не можете її змінити, то так, сингл може бути хорошим рішенням.
Шепмайстер

3
@Worik Ви б хотіли пояснити, чому? Я заважаю людям робити щось, що є поганою ідеєю на більшості мов (навіть ОП погодилася, що глобальний був поганим вибором для їх застосування). Ось що взагалі означає. Потім я показую два рішення, як це зробити в будь-якому випадку. Я щойно тестував lazy_staticприклад в Rust 1.24.1, і він працює точно. Тут external staticніде немає . Можливо, вам потрібно перевірити речі на своєму кінці, щоб переконатися, що ви зрозуміли відповідь повністю.
Шепмайстер

1
@Worik, якщо вам потрібна допомога з основ використання ящика, я пропоную вам перечитати мову програмування Rust . У главі про створення гри на здогадки показано, як додати залежності.
Шепмейстер

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(
        // ...
    );
}

Якщо ви хочете змінити стан (НЕ Сінглтон), див. Що не робити в іржі для отримання додаткових описів.

Сподіваюся, що це корисно.


-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. Сенс маркування речей як дублікатів на ПЗ полягає в тому, щоб не мати зайвої інформації.
Шепмейстер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.