web-dev-qa-db-fra.com

Comment créer un singleton global et mutable?

Quel est le meilleur moyen de créer et d'utiliser une structure avec une seule instanciation dans le système? Oui, cela est nécessaire, il s’agit du sous-système OpenGL, et en faire plusieurs copies et le faire circuler partout ajouterait à la confusion, au lieu de le soulager.

Le singleton doit être aussi efficace que possible. Il ne semble pas possible de stocker un objet arbitraire dans la zone statique, car il contient un Vec avec un destructeur. La deuxième option consiste à stocker un pointeur (non sécurisé) sur la zone statique, en pointant sur un singleton attribué au segment de mémoire. Quel est le moyen le plus pratique et le plus sûr de le faire, tout en conservant une syntaxe concise.

92
stevenkucera

Réponse non-réponse

Évitez l'état global en général. Construisez plutôt l’objet quelque part tôt (peut-être dans main), puis transmettez les références modifiables à cet objet aux endroits qui en ont besoin. Cela rendra généralement votre code plus facile à raisonner et n'exigera pas autant de déformation en arrière.

Regardez-vous bien dans le miroir avant de décider que vous voulez des variables mutables globales. Il y a de rares cas où cela est utile, c'est pourquoi il est important de savoir comment s'y prendre.

Toujours envie d'en faire un ...?

Utiliser paresseux-statique

La caisse lazy-static peut vous éviter un peu de corvée de créer un singleton (ci-dessous). Voici un vecteur global modifiable:

#[macro_use]
extern crate lazy_static;

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 Mutex, vous obtenez un singleton global sans aucune mutabilité.

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 dû à l'implémentation de stdin de = (= Rust 1.0). Vous devriez également regarder l'implémentation moderne de io::Lazy . J'ai commenté en ligne avec ce que chaque ligne fait.

use std::sync::{Arc, Mutex, Once, ONCE_INIT};
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_INIT;

    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 est compilé avec Rust 1.23.0. Les vraies implémentations de Stdin utilisent certaines fonctionnalités instables pour tenter de libérer la mémoire allouée, ce que ce code ne permet pas.

Vraiment, vous voudrez probablement faire SingletonReader mettre en œuvre Deref et DerefMut afin que vous n'ayez pas à le faire piquez dans l'objet et verrouillez-le vous-même.

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

Le sens de "global"

Veuillez noter que vous pouvez toujours utiliser la confidentialité normale Rust et la confidentialité au niveau du module pour contrôler l’accès à static ou lazy_static variable. 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ù il en existe une instance dans l'ensemble du programme.

110
Shepmaster