Comment créer un singleton global et mutable?

142

Quelle est la meilleure façon de créer et d'utiliser une structure avec une seule instanciation dans le système? Oui, c'est nécessaire, c'est le sous-système OpenGL, et faire de multiples copies de celui-ci et le faire circuler partout ajouterait de la confusion, plutôt que de la soulager.

Le singleton doit être aussi efficace que possible. Il ne semble pas possible de stocker un objet arbitraire sur la zone statique, car il contient un Vecavec un destructeur. La deuxième option consiste à stocker un pointeur (non sécurisé) sur la zone statique, pointant vers un singleton alloué au tas. Quelle est la façon la plus pratique et la plus sûre de le faire, tout en gardant une syntaxe laconique.

stevenkucera
la source
1
Avez-vous regardé comment les liaisons Rust existantes pour OpenGL gèrent ce même problème?
Shepmaster
20
Oui, c'est nécessaire, c'est le sous-système OpenGL, et faire de multiples copies de celui-ci et le faire circuler partout ajouterait de la confusion, plutôt que de la soulager. => ce n'est pas la définition du nécessaire , c'est peut-être pratique (au début) mais pas nécessaire.
Matthieu M.
3
Oui, vous avez un point. Bien qu'OpenGL soit de toute façon une grosse machine à états, je suis proche de la certitude qu'il n'y en aura aucun clone nulle part, dont l'utilisation n'entraînerait que des erreurs OpenGL.
stevenkucera

Réponses:

200

Réponse sans réponse

Évitez l'état global en général. Au lieu de cela, construisez l'objet quelque part tôt (peut-être dans main), puis transmettez les références mutables à cet objet aux endroits qui en ont besoin. Cela rendra généralement votre code plus facile à raisonner et ne nécessitera pas autant de flexion en arrière.

Regardez-vous attentivement dans le miroir avant de décider que vous voulez des variables globales mutables. Il y a de rares cas où cela est utile, c'est pourquoi cela vaut la peine de savoir comment faire.

Vous voulez toujours en faire un ...?

Utiliser lazy-static

La caisse paresseuse-statique peut éliminer une partie de la corvée de la création manuelle d'un singleton. Voici un vecteur mutable global:

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

Si vous supprimez le, Mutexvous avez un singleton global sans aucune mutabilité.

Vous pouvez également utiliser a RwLockau lieu de a Mutexpour autoriser plusieurs lecteurs simultanés.

Utiliser once_cell

La caisse once_cell peut éliminer une partie de la corvée de la création manuelle d'un singleton. Voici un vecteur mutable global:

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

Si vous supprimez le, Mutexvous avez un singleton global sans aucune mutabilité.

Vous pouvez également utiliser a RwLockau lieu de a Mutexpour autoriser plusieurs lecteurs simultanés.

Un cas particulier: l'atomique

Si vous avez seulement besoin de suivre une valeur entière, vous pouvez directement utiliser un atomique :

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

Implémentation manuelle sans dépendance

Ceci est grandement lié à l'implémentation de Rust 1.0stdin avec quelques modifications pour Rust moderne. Vous devriez également examiner l'implémentation moderne de io::Lazy. J'ai commenté en ligne avec ce que fait chaque ligne.

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

Cela imprime:

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

Ce code compile avec Rust 1.42.0. Les vraies implémentations Stdinutilisent certaines fonctionnalités instables pour tenter de libérer la mémoire allouée, ce que ce code ne fait pas.

Vraiment, vous voudriez probablement créer un SingletonReaderimplémentation Derefet DerefMutvous n'avez donc pas besoin de pousser l'objet et de le verrouiller vous-même.

Tout ce travail est ce que lazy-static ou once_cell fait pour vous.

La signification de «global»

Veuillez noter que vous pouvez toujours utiliser l'étendue Rust normale et la confidentialité au niveau du module pour contrôler l'accès à une variable staticou lazy_static. Cela signifie que vous pouvez le déclarer dans un module ou même à l'intérieur d'une fonction et qu'il ne sera pas accessible en dehors de ce module / fonction. C'est bon pour contrôler l'accès:

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

Cependant, la variable est toujours globale dans la mesure où une instance de celle-ci existe dans tout le programme.

Shepmaster
la source
72
Après beaucoup de réflexion, je suis convaincu de ne pas utiliser le Singleton, mais plutôt de n'utiliser aucune variable globale et de tout transmettre. Rend le code plus auto-documenté car il est clair quelles fonctions accèdent au moteur de rendu. Si je veux revenir au singleton, ce sera plus facile de le faire que l'inverse.
stevenkucera
4
Merci pour la réponse, cela a beaucoup aidé. J'ai juste pensé laisser ici un commentaire pour décrire ce que je considère comme un cas d'utilisation valide pour lazy_static !. Je l'utilise pour s'interfacer avec une application C qui permet de charger / décharger des modules (objets partagés) et le code rust est l'un de ces modules. Je ne vois pas beaucoup d'options que d'utiliser un global on load parce que je n'ai aucun contrôle sur main () et comment l'application principale s'interface avec mon module. J'avais essentiellement besoin d'un vecteur de choses qui peuvent être ajoutées à l'exécution après le chargement de mon mod.
Moises Silva
1
@MoisesSilva il y aura toujours une raison d'avoir besoin d'un singleton, mais il est inutile de l'utiliser dans de nombreux cas où il est utilisé. Sans connaître votre code, il est possible que l'application C permette à chaque module de renvoyer une "donnée utilisateur" void *qui est ensuite renvoyée dans les méthodes de chaque module. Il s'agit d'un modèle d'extension typique pour le code C. Si l'application ne le permet pas et que vous ne pouvez pas le modifier, alors oui, un singleton peut être une bonne solution.
Shepmaster
3
@Worik voudriez-vous expliquer pourquoi? Je décourage les gens de faire quelque chose qui est une mauvaise idée dans la plupart des langues (même le PO a convenu qu'un global était un mauvais choix pour leur application). C'est ce que signifie en général . Je montre ensuite deux solutions pour savoir comment le faire de toute façon. Je viens de tester l' lazy_staticexemple dans Rust 1.24.1 et cela fonctionne exactement. Il n'y a external staticnulle part ici. Vous devez peut-être vérifier les choses de votre côté pour vous assurer que vous avez bien compris la réponse.
Shepmaster
1
@Worik si vous avez besoin d'aide pour savoir comment utiliser une caisse, je vous suggère de relire The Rust Programming Language . Le chapitre sur la création d'un jeu de devinettes montre comment ajouter des dépendances.
Shepmaster
0

Utilisez SpinLock pour un accès global.

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

Si vous voulez un état mutable (PAS Singleton), consultez Que ne pas faire dans Rust pour plus de descriptions.

J'espère que c'est utile.

codeur débranché
la source
-1

Répondre à ma propre question en double .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Crate root (lib.rs):

#[macro_use]
extern crate lazy_static;

Initialisation (pas besoin de bloc non sécurisé):

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

ÉDITER:

J'ai réussi à le résoudre avec once_cell, qui n'a pas besoin de 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
});
easychessanimations
la source
2
Cette réponse n'apporte rien de nouveau par rapport aux réponses existantes, qui discutent déjà lazy_staticet les plus récentes once_cell. Le point de marquer des choses comme des doublons sur SO est d'éviter d'avoir des informations redondantes.
Shepmaster