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

2024-04-15

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

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


无答案的答案

一般来说,避免全局状态。相反,尽早在某个地方构建对象(也许在main),然后将该对象的可变引用传递到需要它的地方。这通常会让你的代码更容易推理,并且不需要太多的向后弯腰。

在决定是否需要全局可变变量之前,请仔细照照镜子。在极少数情况下它是有用的,因此值得了解如何做。

还想做一个吗...?

Tips

在以下解决方案中:

  • 如果您删除Mutex https://doc.rust-lang.org/std/sync/struct.Mutex.html那么你有一个全局单例,没有任何可变性.
  • 您还可以使用RwLock https://doc.rust-lang.org/std/sync/struct.RwLock.html代替Mutex to 允许多个并发读者.

Using lazy-static

The 惰性静态 https://crates.io/crates/lazy_staticcrate 可以消除手动创建单例的一些苦差事。这是一个全局可变向量:

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

Using once_cell

The 一次细胞 https://crates.io/crates/once_cellcrate 可以消除手动创建单例的一些苦差事。这是一个全局可变向量:

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

Using std::sync::LazyLock

标准库位于过程 https://github.com/rust-lang/rust/issues/74465添加的once_cell的功能,目前称为LazyLock https://doc.rust-lang.org/nightly/std/sync/struct.LazyLock.html:

#![feature(once_cell)] // 1.67.0-nightly
use std::sync::{LazyLock, Mutex};

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

Using std::sync::OnceLock

LazyLock仍然不稳定,但是OnceLock https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html从 Rust 1.70.0 开始稳定。您可以使用它在稳定版上获得无依赖实现:

use std::sync::{OnceLock, Mutex};

fn array() -> &'static Mutex<Vec<u8>> {
    static ARRAY: OnceLock<Mutex<Vec<u8>>> = OnceLock::new();
    ARRAY.get_or_init(|| 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());
}

一个特例:原子

如果你只需要跟踪一个整数值,你可以直接使用atomic https://doc.rust-lang.org/std/sync/atomic/:

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

手动、无依赖性实施

有几种现有的静态实现,例如Rust 1.0 的实现stdin https://github.com/rust-lang/rust/blob/2a8cb678e61e91c160d80794b5fdd723d0d4211c/src/libstd/io/stdio.rs#L217-L247。这与适应现代 Rust 的想法相同,例如使用MaybeUninit以避免分配和不必要的间接。您还应该看看现代的实现io::Lazy https://github.com/rust-lang/rust/blob/1.42.0/src/libstd/io/lazy.rs。我已经对每一行的作用进行了内联评论。

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 范围和模块级隐私来控制对static or lazy_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

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何创建一个全局的、可变的单例? 的相关文章

随机推荐