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

ste*_*era 97 rust

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

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

She*_*ter 122

非答案答案

一般避免全球状态.相反,在早期(可能在main)的某处构建对象,然后将对该对象的可变引用传递到需要它的位置.这通常会使您的代码更易于推理,并且不需要向后弯曲.

在决定你想要全局可变变量之前,先在镜子里仔细看看.在极少数情况下它很有用,所以这就是为什么它值得知道该怎么做.

还想做一个......?

使用惰性静态

慵懒的静电箱可以带走一些创造了单(下图)的苦差事.这是一个全局可变向量:

#[macro_use]
extern crate lazy_static;

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());
}
Run Code Online (Sandbox Code Playgroud)

如果你删除了Mutex那么你有一个没有任何可变性的全局单例.

一个特例:原子

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

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));
}
Run Code Online (Sandbox Code Playgroud)

手动,无依赖性实施

这很大程度上来自于Rust 1.0的实现stdin.你还应该看看现代的实现io::Lazy.我已经评论了每行的内容.

use std::sync::{Arc, Mutex, Once, ONCE_INIT};
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_INIT;

    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();
    }
}
Run Code Online (Sandbox Code Playgroud)

打印出:

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
Run Code Online (Sandbox Code Playgroud)

此代码使用Rust 1.23.0编译.真正的实现Stdin使用一些不稳定的功能来尝试释放分配的内存,这个代码没有.

真的,你可能想要制作SingletonReader工具Deref,DerefMut所以你不必戳入对象并自己锁定它.

所有这些工作都是lazy-static为您所做的.

  • 经过深思熟虑后,我确信不使用Singleton,而是根本不使用全局变量并传递所有内容.使代码更加自我记录,因为很清楚哪些函数访问渲染器.如果我想改回单身,那么比其他方式更容易做到这一点. (49认同)
  • @Worik 你愿意解释一下原因吗?我不鼓励人们在大多数语言中做一些糟糕的事情(甚至 OP 也同意全局对于他们的应用程序来说是一个糟糕的选择)。这就是**一般**的意思。然后我展示了两种解决方案,以说明如何做到这一点。我刚刚在 Rust 1.24.1 中测试了 `lazy_static` 示例,它完全有效。这里没有任何“外部静态”。也许您需要检查一下自己的情况,以确保您已经完全理解了答案。 (5认同)
  • 谢谢你的答案,它帮助了很多.我以为我会在这里发表评论来描述我认为lazy_static的有效用例!我正在使用它来连接到允许加载/卸载模块(共享对象)的C应用程序,并且生锈代码是这些模块之一.我没有看到比使用全局加载更多选项,因为我根本无法控制main()以及核心应用程序如何与我的模块接口.我基本上需要一个可以在我的mod加载后在运行时添加的东西的向量. (4认同)
  • 这不是一个好的答案。“一般来说要避免全局状态”,但全局状态是一个事物并且需要表示。并且“external static”的代码存在缺陷,无法在 rustc 1.24.1 上编译 (4认同)
  • 是的,传递上下文是可行的,但这是一个大型应用程序,我们实际上没有太多控制权,并且更改模块的接口将意味着更新数百个第三方模块或创建新的模块 API,这两种更改都涉及比只是使用lazy-static编写一个插件模块。 (3认同)
  • @Worik如果您需要有关如何使用板条箱的基础知识的帮助,我建议您重新阅读[*Rust 编程语言*](https://doc.rust-lang.org/book/second-edition/) 。[创建猜谜游戏一章](https://doc.rust-lang.org/book/second-edition/ch02-00-guessing-game-tutorial.html)展示了如何添加依赖项。 (2认同)
  • @Rokit总是有例外,但是是的,我想说在库中拥有全局可变变量通常是一个坏主意。这些阻碍了在多个不同的并发上下文中轻松使用该库。不可变的全局变量没那么糟糕(例如编译后的“Regex”)。 (2认同)

Dan*_*ger 28

从 Rust 1.63 开始,使用全局可变单例会更容易,尽管在大多数情况下仍然最好避免全局变量。

现在,您可以使用全局静态锁Mutex::new,而无需延迟初始化:constMutex

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());
}
Run Code Online (Sandbox Code Playgroud)

请注意,这还取决于 的Vec::new事实const。如果您需要使用非const函数来设置单例,您可以将数据包装在 中Option,并最初将其设置为NoneHashset这允许您使用当前无法在上下文中使用的数据结构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());
}
Run Code Online (Sandbox Code Playgroud)

或者,您可以使用RwLock代替 a Mutex,因为从 Rust 1.63 开始也是如此RwLock::newconst这使得同时从多个线程读取数据成为可能。

如果您需要使用非const函数进行初始化并且您不希望使用 an ,则可以使用像once_celllazy-staticOption这样的包来进行延迟初始化,如Shepmaster 的回答中所述。


小智 6

《Rust 中不该做的事》开始

回顾一下:不要使用对象更改其内部状态的内部可变性,而是考虑使用一种模式,将新状态提升为当前状态,并且旧状态的当前使用者将通过将 Arc 放入 RwLock 来继续保留它。

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
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 您好,请参阅[这个问题](/sf/ask/4816492481/),因为我不确定“thread_local”是否正确,因为它将创建 `Arc&lt;Config&gt;` 的多个实例(每个正在运行的线程一个)。 (7认同)