¿Cómo creo un singleton mutable global?


140

¿Cuál es la mejor manera de crear y usar una estructura con solo una instanciación en el sistema? Sí, esto es necesario, es el subsistema OpenGL, y hacer múltiples copias de esto y pasarlo por todas partes agregaría confusión, en lugar de aliviarla.

El singleton debe ser lo más eficiente posible. No parece posible almacenar un objeto arbitrario en el área estática, ya que contiene un Veccon un destructor. La segunda opción es almacenar un puntero (inseguro) en el área estática, apuntando a un singleton asignado al montón. ¿Cuál es la forma más conveniente y segura de hacer esto, manteniendo la sintaxis concisa?


1
¿Ha observado cómo los enlaces Rust existentes para OpenGL manejan este mismo problema?
Shepmaster

20
Sí, esto es necesario, es el subsistema OpenGL, y hacer múltiples copias de esto y pasarlo por todas partes agregaría confusión, en lugar de aliviarla. => esta no es la definición de necesario , tal vez sea conveniente (al principio) pero no necesario.
Matthieu M.

3
Sí, tienes razón. Aunque, dado que OpenGL es una gran máquina de estado de todos modos, estoy casi seguro de que no habrá un clon de él en ninguna parte, cuyo uso solo resultaría en errores de OpenGL.
stevenkucera

Respuestas:


198

Respuesta sin respuesta

Evite el estado global en general. En su lugar, construya el objeto en algún lugar temprano (tal vez adentro main), luego pase referencias mutables a ese objeto en los lugares que lo necesitan. Por lo general, esto hará que sea más fácil razonar sobre el código y no requerirá tanto hacer lo imposible.

Mírese con atención en el espejo antes de decidir que quiere variables mutables globales. Hay casos raros en los que es útil, por eso vale la pena saber cómo hacerlo.

¿Todavía quieres hacer uno ...?

Usando lazy-static

La caja estática perezosa puede eliminar parte del trabajo pesado de crear manualmente un singleton. Aquí hay un vector 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 elimina el, Mutexentonces tiene un singleton global sin ninguna mutabilidad.

También puede utilizar a en RwLocklugar de a Mutexpara permitir varios lectores simultáneos.

Usando once_cell

La caja once_cell puede eliminar parte de la monotonía de crear manualmente un singleton. Aquí hay un vector 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 elimina el, Mutexentonces tiene un singleton global sin ninguna mutabilidad.

También puede utilizar a en RwLocklugar de a Mutexpara permitir varios lectores simultáneos.

Un caso especial: atómico

Si solo necesita rastrear un valor entero, puede usar directamente un valor atómico :

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

Implementación manual y sin dependencias

Esto se debe en gran medida a la implementación de Rust 1.0stdin con algunos ajustes para el Rust moderno. También debe mirar la implementación moderna de io::Lazy. He comentado en línea con lo que hace cada línea.

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

Esto 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

Este código se compila con Rust 1.42.0. Las implementaciones reales de Stdinusan algunas características inestables para intentar liberar la memoria asignada, lo que este código no hace.

Realmente, probablemente querrá hacer un SingletonReaderimplemento Derefy, DerefMutpor lo tanto, no tendrá que empujar el objeto y bloquearlo usted mismo.

Todo este trabajo es lo que lazy-static o once_cell hacen por ti.

El significado de "global"

Tenga en cuenta que aún puede usar el alcance normal de Rust y la privacidad a nivel de módulo para controlar el acceso a una variable statico lazy_static. Esto significa que puede declararlo en un módulo o incluso dentro de una función y no será accesible fuera de ese módulo / función. Esto es bueno para controlar el acceso:

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

Sin embargo, la variable sigue siendo global en el sentido de que hay una instancia de ella que existe en todo el programa.


72
Después de pensarlo mucho, estoy convencido de no usar Singleton y, en su lugar, no usar ninguna variable global y pasar todo. Hace que el código sea más autodocumentado, ya que está claro qué funciones acceden al renderizador. Si quiero volver a cambiar a singleton, será más fácil hacerlo que al revés.
stevenkucera

4
Gracias por la respuesta, ayudó mucho. ¡Solo pensé en dejar aquí un comentario para describir lo que veo como un caso de uso válido para lazy_static !. Lo estoy usando para interactuar con una aplicación C que permite cargar / descargar módulos (objetos compartidos) y el código rust es uno de estos módulos. No veo muchas opciones que usar un global on load porque no tengo ningún control sobre main () y cómo la aplicación principal interactúa con mi módulo. Básicamente, necesitaba un vector de cosas que se pueden agregar en tiempo de ejecución después de que se cargue mi mod.
Moises Silva

1
@MoisesSilva siempre habrá alguna razón para necesitar un singleton, pero es innecesario usarlo en muchos de los casos en que se usa. Sin conocer su código, es posible que la aplicación C permita que cada módulo devuelva "datos de usuario" void *que luego se devuelven a los métodos de cada módulo. Este es un patrón de extensión típico para el código C. Si la aplicación no lo permite y no puede cambiarlo, entonces sí, un singleton puede ser una buena solución.
Shepmaster

3
@Worik, ¿te importaría explicar por qué? Desaliento a la gente a hacer algo que sea una mala idea en la mayoría de los idiomas (incluso el OP estuvo de acuerdo en que un global era una mala elección para su aplicación). Eso es lo que significa en general . Luego muestro dos soluciones sobre cómo hacerlo de todos modos. Acabo de probar el lazy_staticejemplo en Rust 1.24.1 y funciona exactamente. No hay ningún external staticlugar aquí. Tal vez necesite verificar las cosas por su parte para asegurarse de que ha entendido la respuesta completamente.
Shepmaster

1
@Worik, si necesita ayuda con los conceptos básicos de cómo usar una caja, le sugiero que vuelva a leer The Rust Programming Language . El capítulo sobre cómo crear un juego de adivinanzas muestra cómo agregar dependencias.
Shepmaster

0

Utilice SpinLock para acceso 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 desea un estado mutable (NO Singleton), consulte Qué no hacer en Rust para obtener más descripciones.

Espero que sea de ayuda.


-1

Respondiendo a mi propia pregunta duplicada .

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

Raíz de caja (lib.rs):

#[macro_use]
extern crate lazy_static;

Inicialización (sin necesidad de bloqueo inseguro):

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

EDITAR:

Se las arregló para resolverlo con once_cell, que no necesita una macro.

Cargo.toml:

[dependencies]
once_cell = "1.3.1"

cuadrados.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
Esta respuesta no aporta nada nuevo en comparación con las respuestas existentes, que ya se discuten lazy_staticy las más nuevas once_cell. El objetivo de marcar cosas como duplicadas en SO es evitar tener información redundante.
Shepmaster
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.