patron example ejercicios ejemplo diseño code rust

rust - example - patron singleton java



¿Cómo se puede hacer un singleton estático seguro en Rust? (2)

Este es un tema controvertido, así que permítame comenzar explicando mi caso de uso y luego hablar sobre el problema real.

Encuentro que para un montón de cosas inseguras, es importante asegurarse de que no pierdas memoria; esto es realmente bastante fácil de hacer si comienzas a usar transmute() y forget() . Por ejemplo, pasar una instancia en caja al código C durante un tiempo arbitrario, luego recuperarlo y "resucitarlo" usando transmute .

Imagina que tengo una envoltura segura para este tipo de API:

trait Foo {} struct CBox; impl CBox { /// Stores value in a bound C api, forget(value) fn set<T: Foo>(value: T) { // ... } /// Periodically call this and maybe get a callback invoked fn poll(_: Box<Fn<(EventType, Foo), ()> + Send>) { // ... } } impl Drop for CBox { fn drop(&mut self) { // Safely load all saved Foo''s here and discard them, preventing memory leaks } }

Para probar esto, en realidad no hay pérdidas de memoria, quiero algunas pruebas como esta:

#[cfg(test)] mod test { struct IsFoo; impl Foo for IsFoo {} impl Drop for IsFoo { fn drop(&mut self) { Static::touch(); } } #[test] fn test_drops_actually_work() { guard = Static::lock(); // Prevent any other use of Static concurrently Static::reset(); // Set to zero { let c = CBox; c.set(IsFoo); c.set(IsFoo); c.poll(/*...*/); } assert!(Static::get() == 2); // Assert that all expected drops were invoked guard.release(); } }

¿Cómo se puede crear este tipo de objeto singleton estático?

Debe usar un bloqueo de seguridad de estilo Semaphore para garantizar que no se ejecuten varias pruebas simultáneamente y luego acceder de forma insegura a algún tipo de valor mutable estático.

Pensé que tal vez esta implementación funcionaría , pero en la práctica falla porque en ocasiones las condiciones de carrera dan como resultado una ejecución duplicada de init :

/// Global instance static mut INSTANCE_LOCK: bool = false; static mut INSTANCE: *mut StaticUtils = 0 as *mut StaticUtils; static mut WRITE_LOCK: *mut Semaphore = 0 as *mut Semaphore; static mut LOCK: *mut Semaphore = 0 as *mut Semaphore; /// Generate instances if they don''t exist unsafe fn init() { if !INSTANCE_LOCK { INSTANCE_LOCK = true; INSTANCE = transmute(box StaticUtils::new()); WRITE_LOCK = transmute(box Semaphore::new(1)); LOCK = transmute(box Semaphore::new(1)); } }

Tenga en cuenta específicamente que, a diferencia de un programa normal en el que puede estar seguro de que su punto de entrada (principal) siempre se ejecuta en una sola tarea, el corredor de prueba en Rust no ofrece ningún tipo de punto de entrada único como este.

Otro, obviamente, que especificar el número máximo de tareas; dadas docenas de pruebas, solo unas pocas necesitan hacer este tipo de cosas, y es lento e inútil limitar el grupo de tareas de prueba a una solo para este caso.


Parece un caso de uso para std::sync::Once :

use std::sync::{Once, ONCE_INIT}; static INIT: Once = ONCE_INIT;

Luego en tus pruebas llama

INIT.doit(|| unsafe { init(); });

Once que se garantiza que su init solo se ejecutará una vez, sin importar cuántas veces llame a INIT.doit() .


Vea también lazy_static , que hace las cosas un poco más ergonómicas. Básicamente, hace lo mismo que una Once estática para cada variable, pero la envuelve en un tipo que implementa Deref para que pueda acceder a ella como una referencia normal.

El uso se ve así ( de la documentación ):

#[macro_use] extern crate lazy_static; use std::collections::HashMap; lazy_static! { static ref HASHMAP: HashMap<u32, &''static str> = { let mut m = HashMap::new(); m.insert(0, "foo"); m.insert(1, "bar"); m.insert(2, "baz"); m }; static ref COUNT: usize = HASHMAP.len(); static ref NUMBER: u32 = times_two(21); } fn times_two(n: u32) -> u32 { n * 2 } fn main() { println!("The map has {} entries.", *COUNT); println!("The entry for `0` is /"{}/".", HASHMAP.get(&0).unwrap()); println!("A expensive calculation on a static results in: {}.", *NUMBER); }

Tenga en cuenta que autoderef significa que ni siquiera tiene que usar * siempre que llame a un método en su variable estática. La variable se inicializará la primera vez que sea Deref ''d.

Sin embargo, las variables lazy_static son inmutables (ya que están detrás de una referencia). Si desea una estática mutable, deberá utilizar un Mutex :

lazy_static! { static ref VALUE: Mutex<u64>; } impl Drop for IsFoo { fn drop(&mut self) { let mut value = VALUE.lock().unwrap(); *value += 1; } } #[test] fn test_drops_actually_work() { // Have to drop the mutex guard to unlock, so we put it in its own scope { *VALUE.lock().unwrap() = 0; } { let c = CBox; c.set(IsFoo); c.set(IsFoo); c.poll(/*...*/); } assert!(*VALUE.lock().unwrap() == 2); // Assert that all expected drops were invoked }