Come si crea un singleton globale e mutevole?


140

Qual è il modo migliore per creare e utilizzare una struttura con una sola istanza nel sistema? Sì, è necessario, è il sottosistema OpenGL, e fare più copie di questo e passarlo ovunque aggiungerebbe confusione, piuttosto che alleviarlo.

Il singleton deve essere il più efficiente possibile. Non sembra possibile memorizzare un oggetto arbitrario nell'area statica, poiché contiene un Veccon un distruttore. La seconda opzione è memorizzare un puntatore (non sicuro) nell'area statica, che punta a un heap allocato singleton. Qual è il modo più comodo e sicuro per farlo, mantenendo la sintassi concisa.


1
Hai visto come le associazioni Rust esistenti per OpenGL gestiscono questo stesso problema?
Shepmaster

20
Sì, è necessario, è il sottosistema OpenGL, e fare più copie di questo e passarlo ovunque aggiungerebbe confusione, piuttosto che alleviarlo. => questa non è la definizione di necessario , forse è conveniente (all'inizio) ma non necessario.
Matthieu M.

3
Sì, hai ragione. Sebbene OpenGL sia comunque una grande macchina a stati, sono quasi certo che non ce ne sarà un clone da nessuna parte, il cui utilizzo provocherebbe solo errori OpenGL.
stevenkucera

Risposte:


198

Risposta senza risposta

Evita lo stato globale in generale. Invece, costruisci l'oggetto da qualche parte all'inizio (forse dentro main), quindi passa i riferimenti mutabili a quell'oggetto nei luoghi che ne hanno bisogno. Questo di solito renderà il tuo codice più facile da ragionare e non richiederà tanto piegarsi all'indietro.

Guardati bene allo specchio prima di decidere che vuoi variabili mutabili globali. Ci sono rari casi in cui è utile, ecco perché vale la pena sapere come fare.

Vuoi ancora farne uno ...?

Utilizzando lazy-static

La cassa pigro-statica può eliminare parte del lavoro ingrato di creare manualmente un singleton. Ecco un vettore mutabile globale:

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

Se rimuovi il, Mutexallora hai un singleton globale senza alcuna mutabilità.

È inoltre possibile utilizzare a RwLockinvece di a Mutexper consentire più lettori simultanei.

Utilizzo di once_cell

La cassa once_cell può eliminare parte della fatica di creare manualmente un singleton. Ecco un vettore mutabile globale:

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

Se rimuovi il, Mutexallora hai un singleton globale senza alcuna mutabilità.

È inoltre possibile utilizzare a RwLockinvece di a Mutexper consentire più lettori simultanei.

Un caso speciale: gli atomici

Se devi solo tenere traccia di un valore intero, puoi utilizzare direttamente un atomico :

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

Implementazione manuale e senza dipendenze

Questo è stato notevolmente modificato dall'implementazione di Rust 1.0stdin con alcune modifiche per Rust moderno. Dovresti anche esaminare l'implementazione moderna di io::Lazy. Ho commentato in linea con ciò che fa ogni riga.

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

Questo stampa:

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

Questo codice viene compilato con Rust 1.42.0. Le reali implementazioni Stdinutilizzano alcune funzionalità instabili per tentare di liberare la memoria allocata, cosa che questo codice non fa.

In realtà, probabilmente avresti voluto creare un SingletonReaderattrezzo Derefe DerefMutquindi non dovevi entrare nell'oggetto e bloccarlo da solo.

Tutto questo lavoro è ciò che lazy-static o once_cell fanno per te.

Il significato di "globale"

Nota che puoi ancora usare il normale scoping Rust e la privacy a livello di modulo per controllare l'accesso a una variabile statico lazy_static. Ciò significa che puoi dichiararlo in un modulo o anche all'interno di una funzione e non sarà accessibile al di fuori di quel modulo / funzione. Questo è utile per controllare l'accesso:

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

Tuttavia, la variabile è ancora globale in quanto ne esiste un'istanza che esiste nell'intero programma.


72
Dopo molte riflessioni sono convinto di non usare il Singleton, e invece di non usare affatto variabili globali e di passare tutto. Rende il codice più auto-documentante poiché è chiaro quali funzioni accedono al renderer. Se voglio tornare al singleton, sarà più facile farlo rispetto al contrario.
stevenkucera

4
Grazie per la risposta, ha aiutato molto. Ho solo pensato di lasciare qui un commento per descrivere quello che vedo come un caso d'uso valido per lazy_static !. Lo sto usando per interfacciarmi con un'applicazione C che consente di caricare / scaricare moduli (oggetti condivisi) e il codice ruggine è uno di questi moduli. Non vedo molte opzioni rispetto all'utilizzo di un globale al caricamento perché non ho alcun controllo su main () e su come l'applicazione principale si interfaccia con il mio modulo. Fondamentalmente avevo bisogno di un vettore di cose che possono essere aggiunte in fase di esecuzione dopo che la mia mod è stata caricata.
Moises Silva

1
@ MoisesSilva ci sarà sempre qualche motivo per aver bisogno di un singleton, ma non è necessario usarlo in molti dei casi in cui viene utilizzato. Senza conoscere il codice, è possibile che l'applicazione C consenta a ciascun modulo di restituire un "dato utente" void *che viene quindi ritrasmesso ai metodi di ciascun modulo. Questo è un tipico modello di estensione per il codice C. Se l'applicazione non lo consente e non è possibile modificarlo, allora sì, un singleton potrebbe essere una buona soluzione.
Shepmaster

3
@Worik, vorresti spiegare perché? Scoraggio le persone dal fare qualcosa che è una cattiva idea nella maggior parte delle lingue (anche l'OP ha convenuto che un globale era una cattiva scelta per la loro applicazione). Questo è ciò che in generale significa. Quindi mostro due soluzioni su come farlo comunque. Ho appena testato l' lazy_staticesempio in Rust 1.24.1 e funziona esattamente. Non c'è nessun external staticposto qui. Forse hai bisogno di controllare le cose da te per assicurarti di aver compreso appieno la risposta.
Shepmaster

1
@Worik se hai bisogno di aiuto con le basi di come usare una cassa, ti suggerisco di rileggere The Rust Programming Language . Il capitolo sulla creazione di un gioco d'ipotesi mostra come aggiungere dipendenze.
Shepmaster

0

Usa SpinLock per l'accesso globale.

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

Se vuoi lo stato mutabile (NON Singleton), vedi Cosa non fare in Rust per ulteriori descrizioni.

Spero sia utile.


-1

Rispondendo alla mia domanda duplicata .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Crate root (lib.rs):

#[macro_use]
extern crate lazy_static;

Inizializzazione (non è necessario un blocco non sicuro):

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

MODIFICARE:

Riuscito a risolverlo con once_cell, che non necessita di macro.

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
Questa risposta non fornisce nulla di nuovo rispetto alle risposte esistenti, che già discutono lazy_statice le più recenti once_cell. Lo scopo di contrassegnare le cose come duplicati su SO è evitare di avere informazioni ridondanti.
Shepmaster
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.