Jak utworzyć globalny, zmienny singleton?


140

Jaki jest najlepszy sposób tworzenia i używania struktury z tylko jedną instancją w systemie? Tak, jest to konieczne, jest to podsystem OpenGL, a tworzenie wielu kopii tego i przekazywanie go wszędzie wprowadziłoby zamieszanie, a nie złagodziło.

Singleton musi być tak wydajny, jak to tylko możliwe. Nie wydaje się możliwe przechowywanie dowolnego obiektu w obszarze statycznym, ponieważ zawiera on a Vecz destruktorem. Drugą opcją jest przechowywanie (niebezpiecznego) wskaźnika w obszarze statycznym, wskazującego na singleton przydzielony do sterty. Jaki jest najwygodniejszy i najbezpieczniejszy sposób, aby to zrobić, zachowując zwięzłą składnię.


1
Czy przyjrzałeś się, jak istniejące powiązania Rust dla OpenGL rozwiązują ten sam problem?
Shepmaster

20
Tak, jest to konieczne, jest to podsystem OpenGL, a tworzenie wielu kopii tego i przekazywanie go wszędzie wprowadziłoby zamieszanie, a nie złagodziło. => to nie jest definicja konieczności , może to być wygodne (na początku), ale niekonieczne.
Matthieu M.

3
Tak, masz rację. Chociaż OpenGL i tak jest dużą maszyną stanów, jestem prawie pewien, że nigdzie nie będzie jego klonu, którego użycie spowodowałoby tylko błędy OpenGL.
stevenkucera

Odpowiedzi:


198

Brak odpowiedzi

Ogólnie unikaj stanu globalnego. Zamiast tego skonstruuj obiekt gdzieś wcześnie (być może w main), a następnie przekaż zmienne odniesienia do tego obiektu w miejsca, które tego potrzebują. Zwykle sprawia to, że twój kod jest łatwiejszy do rozważenia i nie wymaga tak dużego pochylania się od tyłu.

Przyjrzyj się sobie uważnie w lustrze, zanim zdecydujesz, że chcesz, aby zmienne globalne były zmienne. Są rzadkie przypadki, w których jest to przydatne, dlatego warto wiedzieć, jak to zrobić.

Nadal chcesz zrobić ...?

Używanie lazy-static

Leniwy-statyczne paka może zabrać niektóre znoju ręcznego tworzenia pojedyncza. Oto globalny zmienny wektor:

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

Jeśli usuniesz, Mutexto masz globalny singleton bez żadnej zmienności.

Możesz także użyć a RwLockzamiast a, Mutexaby zezwolić wielu jednoczesnym czytnikom.

Korzystanie z Once_cell

Once_cell paka może zabrać niektóre znoju ręcznego tworzenia pojedyncza. Oto globalny zmienny wektor:

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

Jeśli usuniesz, Mutexto masz globalny singleton bez żadnej zmienności.

Możesz także użyć a RwLockzamiast a, Mutexaby zezwolić wielu jednoczesnym czytnikom.

Szczególny przypadek: atomika

Jeśli chcesz śledzić tylko wartość całkowitą, możesz bezpośrednio użyć atomowej :

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));
}

Ręczna, wolna od zależności implementacja

Jest to w dużej mierze zaczerpnięte z implementacji Rusta 1.0stdin z kilkoma poprawkami dla współczesnego Rusta. Warto również przyjrzeć się nowoczesnej implementacji io::Lazy. Skomentowałem w tekście, co robi każda linia.

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

To drukuje:

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

Ten kod kompiluje się z Rust 1.42.0. Rzeczywiste implementacje Stdinużywają pewnych niestabilnych funkcji, aby spróbować zwolnić przydzieloną pamięć, czego ten kod nie robi.

Naprawdę, to prawdopodobnie chcesz, aby SingletonReaderwdrożyć Derefi DerefMuttak nie trzeba kłuć do obiektu i zablokować go samodzielnie.

Cała ta praca jest tym, co robi dla ciebie lazy-static lub once_cell.

Znaczenie słowa „globalny”

Pamiętaj, że nadal możesz używać normalnego zakresu Rusta i prywatności na poziomie modułu, aby kontrolować dostęp do zmiennej staticlub lazy_static. Oznacza to, że możesz zadeklarować go w module lub nawet wewnątrz funkcji i nie będzie on dostępny poza tym modułem / funkcją. Jest to dobre do kontrolowania dostępu:

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

Jednak zmienna jest nadal globalna, ponieważ istnieje jej jedno wystąpienie, które istnieje w całym programie.


72
Po wielu przemyśleniach jestem przekonany, że nie powinienem używać Singletona, a zamiast tego nie używać żadnych zmiennych globalnych i przekazywać wszystko. Sprawia, że ​​kod jest bardziej samodokumentujący, ponieważ jest jasne, które funkcje uzyskują dostęp do mechanizmu renderującego. Jeśli chcę wrócić do singletona, będzie to łatwiejsze niż na odwrót.
stevenkucera

4
Dzięki za odpowiedź, bardzo pomogła. Pomyślałem, że zostawię tutaj komentarz opisujący to, co uważam za ważny przypadek użycia lazy_static !. Używam go do połączenia z aplikacją C, która umożliwia ładowanie / wyładowywanie modułów (współdzielonych obiektów), a kod rdzy jest jednym z tych modułów. Nie widzę zbyt wiele opcji niż używanie globalnego przy ładowaniu, ponieważ nie mam żadnej kontroli nad main () i tym, jak podstawowa aplikacja współpracuje z moim modułem. Po prostu potrzebowałem wektora rzeczy, które można dodać w czasie wykonywania po załadowaniu mojego moda.
Moises Silva

1
@MoisesSilva zawsze będzie jakiś powód, aby potrzebować singletona, ale w wielu przypadkach nie trzeba go używać. Bez znajomości kodu możliwe jest, że aplikacja C powinna pozwolić każdemu modułowi na zwrócenie „danych użytkownika”, void *które są następnie przekazywane z powrotem do metod każdego modułu. Jest to typowy wzorzec rozszerzenia dla kodu C. Jeśli aplikacja na to nie pozwala i nie możesz tego zmienić, to tak, singleton może być dobrym rozwiązaniem.
Shepmaster

3
@Worik czy chciałbyś wyjaśnić dlaczego? Odradzam ludziom robienie czegoś, co jest kiepskim pomysłem w większości języków (nawet OP zgodził się, że globalny był złym wyborem dla ich aplikacji). To ogólnie oznacza. Następnie pokazuję dwa rozwiązania, jak to zrobić. Właśnie przetestowałem lazy_staticprzykład w Rust 1.24.1 i działa dokładnie. external staticNigdzie tu nie ma . Być może musisz sprawdzić rzeczy po swojej stronie, aby upewnić się, że w pełni zrozumiałeś odpowiedź.
Shepmaster

1
@Worik Jeśli potrzebujesz pomocy z podstawami korzystania ze skrzynki, proponuję ponownie przeczytać The Rust Programming Language . Rozdział o tworzeniu gra w zgadywanie pokazuje, jak dodać zależności.
Shepmaster

0

Użyj SpinLock, aby uzyskać dostęp globalny.

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

Jeśli chcesz zmieniać stan (NIE Singleton), zobacz Czego nie robić w Rust, aby uzyskać więcej opisów.

Mam nadzieję, że to pomocne.


-1

Odpowiadając na moje zduplikowane pytanie .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Katalog główny skrzynki (lib.rs):

#[macro_use]
extern crate lazy_static;

Inicjalizacja (nie ma potrzeby stosowania niebezpiecznego bloku):

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

EDYTOWAĆ:

Udało się go rozwiązać za pomocą Once_cell, który nie potrzebuje makra.

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
Ta odpowiedź nie dostarcza niczego nowego w porównaniu z dotychczasowymi odpowiedziami, o których już dyskutujemy lazy_statici nowszymi once_cell. Celem oznaczania rzeczy jako duplikatów w SO jest uniknięcie nadmiarowych informacji.
Shepmaster
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.