Wie erstelle ich einen globalen, veränderlichen Singleton?

138

Was ist der beste Weg, um eine Struktur mit nur einer Instanziierung im System zu erstellen und zu verwenden? Ja, dies ist notwendig, es ist das OpenGL-Subsystem, und das Erstellen mehrerer Kopien davon und das Weitergeben überall würde Verwirrung stiften, anstatt es zu lindern.

Der Singleton muss so effizient wie möglich sein. Es scheint nicht möglich zu sein, ein beliebiges Objekt im statischen Bereich zu speichern, da es ein Objekt Vecmit einem Destruktor enthält. Die zweite Option besteht darin, einen (unsicheren) Zeiger auf dem statischen Bereich zu speichern, der auf einen dem Heap zugewiesenen Singleton zeigt. Was ist der bequemste und sicherste Weg, dies zu tun, während die Syntax knapp bleibt?

Stevenkucera
quelle
1
Haben Sie sich angesehen, wie die vorhandenen Rust-Bindungen für OpenGL dasselbe Problem behandeln?
Shepmaster
20
Ja, dies ist notwendig, es ist das OpenGL-Subsystem, und das Erstellen mehrerer Kopien davon und das Weitergeben überall würde Verwirrung stiften, anstatt es zu lindern. => Dies ist nicht die Definition von notwendig , es ist vielleicht bequem (zuerst), aber nicht notwendig.
Matthieu M.
3
Ja, du hast einen Punkt. Obwohl OpenGL ohnehin eine große Zustandsmaschine ist, bin ich mir fast sicher, dass es nirgendwo einen Klon davon geben wird, dessen Verwendung nur zu OpenGL-Fehlern führen würde.
Stevenkucera

Antworten:

195

Antwort ohne Antwort

Vermeiden Sie den globalen Zustand im Allgemeinen. Konstruieren Sie das Objekt stattdessen irgendwo früh (möglicherweise in main) und übergeben Sie dann veränderbare Verweise auf dieses Objekt an die Stellen, an denen es benötigt wird. Dies erleichtert normalerweise das Nachdenken über Ihren Code und erfordert nicht so viel Rückbiegen.

Schauen Sie sich im Spiegel genau an, bevor Sie sich für globale veränderbare Variablen entscheiden. Es gibt seltene Fälle, in denen es nützlich ist. Deshalb lohnt es sich zu wissen, wie es geht.

Willst du noch einen machen ...?

Lazy-Static verwenden

Die faul-statische Kiste kann einen Teil der Plackerei beim manuellen Erstellen eines Singletons beseitigen. Hier ist ein global veränderlicher Vektor:

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

Wenn Sie das entfernen Mutex, haben Sie einen globalen Singleton ohne Veränderbarkeit.

Sie können auch a RwLockanstelle von a verwenden Mutex, um mehrere gleichzeitige Leser zuzulassen.

Einmal_Zelle verwenden

Die Once_cell- Kiste kann die mühsame manuelle Erstellung eines Singletons entlasten . Hier ist ein global veränderlicher Vektor:

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

Wenn Sie das entfernen Mutex, haben Sie einen globalen Singleton ohne Veränderbarkeit.

Sie können auch a RwLockanstelle von a verwenden Mutex, um mehrere gleichzeitige Leser zuzulassen.

Ein Sonderfall: Atomik

Wenn Sie nur einen ganzzahligen Wert verfolgen müssen, können Sie direkt ein Atom verwenden :

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

Manuelle, abhängigkeitsfreie Implementierung

Dies ist stark von der Rust 1.0-Implementierungstdin mit einigen Verbesserungen für das moderne Rust geprägt. Sie sollten sich auch die moderne Implementierung von ansehen io::Lazy. Ich habe inline kommentiert, was jede Zeile tut.

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

Dies druckt aus:

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

Dieser Code wird mit Rust 1.42.0 kompiliert. Die realen Implementierungen Stdinverwenden einige instabile Funktionen, um zu versuchen, den zugewiesenen Speicher freizugeben, was dieser Code nicht tut.

Wirklich, Sie möchten wahrscheinlich ein SingletonReaderGerät erstellen, Derefund müssen DerefMutdaher nicht in das Objekt eindringen und es selbst sperren.

All diese Arbeiten erledigen Lazy-Static oder Once_cell für Sie.

Die Bedeutung von "global"

Bitte beachten Sie, dass Sie weiterhin den normalen Rust-Bereich und die Privatsphäre auf Modulebene verwenden können, um den Zugriff auf eine staticoder eine lazy_staticVariable zu steuern . Dies bedeutet, dass Sie es in einem Modul oder sogar innerhalb einer Funktion deklarieren können und außerhalb dieses Moduls / dieser Funktion nicht darauf zugreifen können. Dies ist gut für die Zugriffskontrolle:

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

Die Variable ist jedoch immer noch global, da eine Instanz davon im gesamten Programm vorhanden ist.

Shepmaster
quelle
72
Nach langem Überlegen bin ich überzeugt, den Singleton nicht zu verwenden, sondern überhaupt keine globalen Variablen zu verwenden und alles weiterzugeben. Macht den Code selbstdokumentierender, da klar ist, welche Funktionen auf den Renderer zugreifen. Wenn ich wieder zu Singleton wechseln möchte, ist dies einfacher als umgekehrt.
Stevenkucera
4
Danke für die Antwort, es hat sehr geholfen. Ich dachte nur, ich würde hier einen Kommentar hinterlassen, um zu beschreiben, was ich als gültigen Anwendungsfall für lazy_static! Ich verwende es als Schnittstelle zu einer C-Anwendung, die das Laden / Entladen von Modulen (gemeinsam genutzten Objekten) ermöglicht, und der Rostcode ist eines dieser Module. Ich sehe nicht viel Option als die Verwendung eines globalen On-Load-Modus, da ich überhaupt keine Kontrolle über main () und die Schnittstelle der Kernanwendung zu meinem Modul habe. Ich brauchte im Grunde einen Vektor von Dingen, die zur Laufzeit hinzugefügt werden können, nachdem mein Mod geladen wurde.
Moises Silva
1
@MoisesSilva Es wird immer einen Grund geben, einen Singleton zu benötigen, aber es ist in vielen Fällen nicht erforderlich, ihn zu verwenden. Ohne Kenntnis Ihres Codes ist es möglich, dass die C-Anwendung jedem Modul erlaubt, "Benutzerdaten" zurückzugeben, void *die dann an die Methoden jedes Moduls zurückgegeben werden. Dies ist ein typisches Erweiterungsmuster für C-Code. Wenn die Anwendung dies nicht zulässt und Sie es nicht ändern können, ist ein Singleton möglicherweise eine gute Lösung.
Shepmaster
3
@Worik würdest du gerne erklären warum? Ich rate den Leuten davon ab, etwas zu tun, was in den meisten Sprachen eine schlechte Idee ist (sogar das OP stimmte zu, dass ein Global eine schlechte Wahl für ihre Anwendung ist). Das bedeutet im Allgemeinen . Ich zeige dann zwei Lösungen, wie es sowieso geht. Ich habe gerade das lazy_staticBeispiel in Rust 1.24.1 getestet und es funktioniert genau. Hier gibt es external staticnirgendwo etwas. Vielleicht müssen Sie die Dinge an Ihrem Ende überprüfen, um sicherzustellen, dass Sie die Antwort vollständig verstanden haben.
Shepmaster
1
@Worik Wenn Sie Hilfe bei den Grundlagen der Verwendung einer Kiste benötigen, empfehlen wir Ihnen, die Programmiersprache The Rust erneut zu lesen . Das Kapitel zum Erstellen eines Ratespiels zeigt, wie Sie Abhängigkeiten hinzufügen.
Shepmaster
0

Verwenden Sie SpinLock für den globalen Zugriff.

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

Wenn Sie einen veränderlichen Status (NICHT Singleton) wünschen , finden Sie weitere Beschreibungen unter Was Sie in Rust nicht tun sollten .

Hoffe es ist hilfreich.

Unpluggedcoder
quelle
-1

Beantwortung meiner eigenen doppelten Frage .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Kistenwurzel (lib.rs):

#[macro_use]
extern crate lazy_static;

Initialisierung (keine Notwendigkeit für einen unsicheren Block):

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

BEARBEITEN:

Es ist gelungen, das Problem mit Once_cell zu lösen, für das kein Makro erforderlich ist.

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
quelle
2
Diese Antwort bietet nichts Neues im Vergleich zu den bereits diskutierten lazy_staticund den neueren Antworten once_cell. Das Markieren von Dingen als Duplikate in SO besteht darin, redundante Informationen zu vermeiden.
Shepmaster