我如何创建一个全局的,可变的单例?

在系统中创建和使用只有一个实例化的结构的最佳方法是什么?是的,这是必要的,它是OpenGL子系统,复制多个副本并到处传递只会增加混乱,而不是缓解混乱。

单例需要尽可能的高效。在静态区域中存储任意对象似乎是不可能的,因为它包含带有析构函数的Vec。第二个选项是在静态区域存储一个(不安全的)指针,指向一个堆分配的单例。在保持语法简洁的同时,最方便、最安全的方法是什么?

131604 次浏览

Non-answer回答

一般避免全局状态。相反,应该在早期的某个地方构造对象(可能在main中),然后将对该对象的可变引用传递到需要它的地方。这通常会使您的代码更容易推理,并且不需要太多的向后弯曲。

在决定使用全局可变变量之前,请仔细照照镜子。在极少数情况下,它是有用的,所以这就是为什么值得知道如何做。

还想做一个吗?

提示

以下3种解决方案:

  • 如果你删除了Mutex,那么你就有了全局单例,没有任何可变性
  • 你也可以使用RwLock来代替Mutex允许多个并发读取器

使用lazy-static

lazy-static板条箱可以消除手动创建单例的一些苦差事。这是一个全局可变向量:

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

使用once_cell

once_cell板条箱可以消除手动创建单例的一些苦差事。这是一个全局可变向量:

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

使用std::sync::SyncLazy

标准库在这个过程中添加了once_cell的功能,目前称为SyncLazy:

#![feature(once_cell)] // 1.53.0-nightly (2021-04-01 d474075a8f28ae9a410e)
use std::{lazy::SyncLazy, sync::Mutex};


static ARRAY: SyncLazy<Mutex<Vec<u8>>> = SyncLazy::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());
}

一个特例:原子学

如果你只需要跟踪一个整数值,你可以直接使用原子:

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

手动、无依赖的实现

静态函数有几个现有的实现,比如stdin的Rust 1.0实现。这是适用于现代Rust的相同思想,例如使用MaybeUninit来避免分配和不必要的间接操作。你还应该看看io::Lazy的现代实现。我用内联注释了每一行的功能。

use std::sync::{Mutex, Once};
use std::time::Duration;
use std::{mem::MaybeUninit, thread};


struct SingletonReader {
// Since we will be used in many threads, we need to protect
// concurrent access
inner: Mutex<u8>,
}


fn singleton() -> &'static SingletonReader {
// Create an uninitialized static
static mut SINGLETON: MaybeUninit<SingletonReader> = MaybeUninit::uninit();
static ONCE: Once = Once::new();


unsafe {
ONCE.call_once(|| {
// Make it
let singleton = SingletonReader {
inner: Mutex::new(0),
};
// Store it to the static var, i.e. initialize it
SINGLETON.write(singleton);
});


// Now we give out a shared reference to the data, which is safe to use
// concurrently.
SINGLETON.assume_init_ref()
}
}


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

打印出来:

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

此代码使用Rust 1.55.0编译。

所有这些工作都是lazy-static或once_cell为您做的。

“全球”的含义;

请注意,你仍然可以使用正常的Rust作用域和模块级隐私来控制对staticlazy_static变量的访问。这意味着你可以在一个模块中甚至在一个函数中声明它,而它在该模块/函数之外是不可访问的。这有利于控制访问:

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

然而,这个变量仍然是全局的,因为它在整个程序中只存在一个实例。

使用自旋锁进行全局访问。

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

如果你想要可变状态(不是单例),请参阅在Rust中不要做什么获得更多描述。

希望对大家有帮助。

在Rust中不要做什么

重述一下:在对象发生变化时使用内部可变性 它的内部状态,考虑使用一个模式来提升new 当前状态为旧状态,当前消费者为旧状态

.

.

.
use std::sync::{Arc, RwLock};


#[derive(Default)]
struct Config {
pub debug_mode: bool,
}


impl Config {
pub fn current() -> Arc<Config> {
CURRENT_CONFIG.with(|c| c.read().unwrap().clone())
}
pub fn make_current(self) {
CURRENT_CONFIG.with(|c| *c.write().unwrap() = Arc::new(self))
}
}


thread_local! {
static CURRENT_CONFIG: RwLock<Arc<Config>> = RwLock::new(Default::default());
}


fn main() {
Config { debug_mode: true }.make_current();
if Config::current().debug_mode {
// do something
}
}

从Rust 1.63开始,可以更容易地使用全局可变单例对象,尽管在大多数情况下避免使用全局变量仍然是可取的。

现在Mutex::newconst,你可以使用全局静态Mutex锁,而不需要延迟初始化:

use std::sync::Mutex;


static GLOBAL_DATA: Mutex<Vec<i32>> = Mutex::new(Vec::new());


fn main() {
GLOBAL_DATA.lock().unwrap().push(42);
println!("{:?}", GLOBAL_DATA.lock().unwrap());
}

注意,这也取决于Vec::newconst这一事实。如果你需要使用非-const函数来设置你的单例,你可以将你的数据包装在Option中,并最初将其设置为None。这允许你使用像Hashset这样的数据结构,目前不能在const上下文中使用:

use std::sync::Mutex;
use std::collections::HashSet;


static GLOBAL_DATA: Mutex<Option<HashSet<i32>>> = Mutex::new(None);


fn main() {
*GLOBAL_DATA.lock().unwrap() = Some(HashSet::from([42]));
println!("V2: {:?}", GLOBAL_DATA.lock().unwrap());
}

或者,你可以使用RwLock,而不是Mutex,因为从Rust 1.63开始,RwLock::new也是const。这样就可以同时从多个线程读取数据。

如果你需要使用非-const函数进行初始化,并且你不喜欢使用Option,你可以使用once_celllazy-static这样的板条箱进行延迟初始化,如Shepmaster的回答中所述。

如果你在每晚上,你可以使用LazyLock

它或多或少做了板条箱once_celllazy_sync所做的事情。这两个板条箱非常常见,所以它们很有可能已经在你的Cargo.lock依赖树中。但如果你喜欢更“冒险”一点;并且使用LazyLock,在它到达稳定的之前,要准备好它(就像每晚中的所有东西一样)可能会发生变化。

(注意:直到最近std::sync::LazyLock被命名为std::lazy::SyncLazy,但最近被命名为重命名。)

有点晚了,但以下是我如何解决这个问题(rust 1.66-nightly):

#![feature(const_size_of_val)]
#![feature(const_ptr_write)]


static mut GLOBAL_LAZY_MUT: StructThatIsNotSyncNorSend = unsafe {
// Copied from MaybeUninit::zeroed() with minor modifications, see below
let mut u = MaybeUninit::uninit();
unsafe {
let bytes = mem::size_of_val(&u);
write_bytes(u.as_ptr() as *const u8 as *mut u8, 0xA5, bytes); //Trick the compiler check that verifies pointers and references are not null.
}


u.assume_init()
};


(...)


fn main() {
unsafe {
let mut v = StructThatIsNotSyncNorSend::new();
mem::swap(&mut GLOBAL_LAZY_MUT, &mut v);
mem::forget(v);
}
  

}

注意,这段代码非常不安全,如果处理不当,很容易变成UB。

你现在有了一个全局静态的!Send !Sync值,没有互斥锁的保护。如果从多个线程访问它,即使只是为了读取,它也是UB。如果你不按显示的方式初始化它,它就是UB,因为它在一个实际的值上调用Drop。

你只是让rust编译器相信某个UB不是UB。你刚刚确信在全局静态中放入!Sync和!Send是可以的。

如果不确定,请不要使用此代码片段。

我有限的解决方案是定义一个结构体,而不是一个全局可变的。要使用该结构体,外部代码需要调用init(),但是通过使用AtomicBoolean(用于多线程使用),我们不允许多次调用init()。

static INITIATED: AtomicBool = AtomicBool::new(false);


struct Singleton {
...
}


impl Singleton {
pub fn init() -> Self {
if INITIATED.load(Ordering::Relaxed) {
panic!("Cannot initiate more than once")
} else {
INITIATED.store(true, Ordering::Relaxed);


Singleton {
...
}
}
}
}


fn main() {
let singleton = Singleton::init();
  

// panic here
// let another_one = Singleton::init();
...
}