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

321

如何创建和使用只有一个实例的结构体是最好的呢?是的,这是必要的,因为它是OpenGL子系统,如果制作多个副本并到处传递会增加混乱而不是减轻。

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


2
你看过现有的 Rust 对 OpenGL 的绑定是如何处理这个问题的吗? - Shepmaster
31
是的,这很必要,它是OpenGL子系统,制作多个副本并在各处传递将增加混乱,而不是减轻它。但这并不是“必要”的定义,可能只是“方便”(起初是这样),但并非必要的。 - Matthieu M.
3
没错,你说得有道理。虽然由于OpenGL本身就是一个大状态机,我几乎可以确定不会有任何克隆版本,使用这些克隆版只会导致OpenGL出现错误。 - stevenkucera
4
@MatthieuM。为了方便起见,这是必要的。 - Asker
9个回答

430

非回答性回答

一般情况下,避免使用全局状态。相反,可以在早期的某个地方(例如在main函数中)构建对象,然后将可变引用传递给需要它的地方。这通常会使您的代码更易于理解,并且不需要过多的弯腰曲背。

在决定是否需要全局可变变量之前,请在镜子前认真审视自己。虽然很少见,但有些情况下它是有用的,所以了解如何使用是值得的。

还是想要创建一个吗...?

提示

在以下解决方案中:

  • 如果移除Mutex,则会得到一个没有任何可变性的全局单例
  • 您还可以使用RwLock代替Mutex,以允许多个并发读取器

使用std::sync::OnceLock

OnceLock 在 Rust 1.70.0 版本中已经稳定下来。您可以使用它来获得一个无依赖的实现:
use std::sync::{Mutex, OnceLock};

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

请注意,LazyLock仍然不稳定,但可以消除array()辅助函数。

使用lazy-static

lazy-static crate可以减少手动创建单例的繁琐工作。这是一个全局可变向量:

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 crate 可以减少手动创建单例的繁琐工作。这里是一个全局可变向量:

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::LazyLock

标准库正在进行中添加once_cell的功能,目前称为LazyLock

#![feature(lazy_cell)]

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

一个特殊情况:原子操作
如果你只需要追踪一个整数值,你可以直接使用一个原子操作。
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实现。这是相同的思路适用于现代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作用域和模块级别的私有性来控制对static或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

然而,尽管如此,这个变量仍然是全局的,也就是说,在整个程序中只存在一个实例。

112
经过深思熟虑,我决定不使用单例模式,而是完全不使用全局变量,并将所有内容传递。这样可以让代码更加易于理解,因为可以清楚地知道哪些函数会访问渲染器。如果我想要回到单例模式,那么相比另一种方法,这种方式更容易实现。 - stevenkucera
7
谢谢您的答复,它帮了我很多。我想留下一条评论,描述我认为懒加载静态变量是有效用例的情况。我正在使用它来与允许加载/卸载模块(共享对象)的C应用程序进行交互,而Rust代码就是这些模块之一。由于我无法控制main()函数以及核心应用程序如何与我的模块进行接口,因此我不太能选择使用全局变量。基本上,在我的模块加载后,我需要一个可以在运行时添加东西的向量。 - Moises Silva
3
是的,传递上下文可能有效,但这是一个大型应用程序,我们并没有太多控制权,并且更改接口以模块方式实现将意味着更新数百个第三方模块或创建新的模块API,这两种更改都涉及比编写使用lazy-static的插件模块更多的工作。 - Moises Silva
3
不是一个好的答案。 "一般情况下应避免全局状态",但全局状态是存在的并需要表示。 而“外部静态”代码存在缺陷,无法在 rustc 1.24.1 上编译。 - Worik
6
@Worik,您能解释一下为什么吗?我劝阻人们不要在大多数语言中使用一个不好的想法(即使 OP 也同意全局变量不适合他们的应用程序)。这就是“通常情况下”的意思。然后我展示了两种解决方案来无论如何完成它。我刚刚在 Rust 1.24.1 中测试了lazy_static示例,它完美地工作了。这里没有任何“外部静态”变量。也许您需要在您的端上检查一下,以确保您已经完全理解了答案。 - Shepmaster
显示剩余18条评论

27
从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的crate进行延迟初始化,如Shepmaster's answer所解释的那样。


这是最简单的解决方案! - undefined

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

7
您好,请查看此问题,因为我不确定thread_local是否正确,因为它会为每个运行的线程创建多个Arc<config>实例。 - thargy
此外,在设计上无法跨线程边界传递的thread_local!存储中使用RwLockArc等线程安全的数据结构是否有点毫无意义呢?在这里,非线程安全版本的RefCellRc是否足够呢? - undefined

3
如果你正在使用 nightly 版本,你可以使用 LazyLock
它基本上做的是类似于 once_celllazy_sync 这两个 crate 的事情。这两个 crate 非常常见,所以很有可能它们已经在你的 Cargo.lock 依赖树中了。但是如果你更喜欢冒险一点,选择使用 LazyLock,请注意它(像所有的 nightly 版本)可能会在进入 stable 版本之前发生变化。
(注:直到最近,std::sync::LazyLock 的名称曾经是 std::lazy::SyncLazy,但最近被 重命名。)

2

使用SpinLock实现全局访问。

#[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中这样做》了解更多信息。希望对您有所帮助。

1
有点晚了,但这是我如何解决这个问题的方法(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();

    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值作为全局静态变量,没有Mutex的保护。如果您从多个线程访问它,即使仅用于读取,也会导致UB。如果您没有按照所示的方式初始化它,那么它就是UB,因为它会在实际上未初始化的值上调用Drop。
您刚刚说服了rust编译器,认为一个UB的东西不是UB。您刚刚相信将!Sync和!Send放在全局静态变量中是可以的。
如果不确定,请不要使用此片段。

1
为什么这不是未定义行为呢?您正在创建一个未初始化的变量,据我所知,即使在适当初始化之前不读取它,这也是瞬间的未定义行为。此外,您在一个“不安全”块中嵌套了另一个“不安全”块(也许是为了强调它真正不安全?) - jthulhu
嵌套的unsafe并不是必要的,那是一个错误。该代码不会有未定义行为,因为在读取之前值被覆盖了(需要在主函数中初始化,在任何竞态条件发生之前)。你可能指的是(即使没有读取)值被覆盖时会出现UB,Rust编译器试图删除旧(未初始化)值。由于这里使用了mem::swap和mem::forget,所以不会发生这种情况。 - Ákos Vandra-Meyer
1
这可能在直觉上是正确的,但即使有一个未初始化的变量,即使您从未访问它,也会导致UB,我认为。请参见文档,特别是与bool相关的示例,似乎与您的片段相匹配。 - jthulhu
我感觉你说的不对,但我也不完全确定。文档中的这两个片段似乎相互矛盾:例如,一个以1初始化的Vec<T>被认为是已初始化的(根据当前实现;这并不构成稳定的保证),因为编译器知道的唯一要求是数据指针必须非空。创建这样的Vec<T>不会立即导致未定义行为,但会导致大多数安全操作(包括丢弃它)出现未定义行为。这意味着将内存初始化为0xA5不会立即导致未定义行为,但是... - Ákos Vandra-Meyer
1
https://users.rust-lang.org/t/is-this-ub-or-not/88739 - Ákos Vandra-Meyer
显示剩余2条评论

0

除了使用第三方的crate,另一种选择是将您的自定义类型(例如结构体)包装在std::cell::Cell中,并放置在std::sync::Mutex内。

  • Mutex 保护自定义类型实例,以便在多线程使用场景下进行并发访问
  • Cell 提供内部可变性,因此从Mutex获取锁的人可以修改自定义类型的内容。

以下是代码示例:

use std::cell::Cell;
use std::sync::Mutex;

#[derive(Debug)]
struct Rectangle {
    width :u16,
    height:u16,
}

static GLOBAL_COUNTER_2: Mutex<Cell<Rectangle>> = 
    Mutex::new(Cell::new(
        Rectangle{width:100u16, height:125u16}
    ));

fn  global_var_demo()
{
    if let Ok(mut currcell) = GLOBAL_COUNTER_2.lock() {
        let mut value = currcell.get_mut();
        value.width += 7;
        value.height = value.height >> 1;
    }
    if let Ok(mut currcell) = GLOBAL_COUNTER_2.lock() {
        // request the reference without moving the ownership
        let value = currcell.get_mut();
        assert_eq!(value.width, 107u16);
        println!("new value in GLOBAL_COUNTER_2: {:?}", value);
    }
}

Mutex已经保证了独占访问并允许您修改其内部内容,Cell是不必要的,这一点可以通过您使用的Cell::get_mut(&mut self)来证明,它已经接受了一个可变引用。 - undefined
谢谢,我发现通过mutex.lock()分配的mut value应该足以修改内部实例。似乎有人因为答案不够精确而给我点了踩。:) - undefined

-1
我已经使用C++相当长的时间了,并且习惯于使用单例模式。我在Rust中实现了这个模式,但当然,它不是线程安全的。如果你需要线程安全,可以使用互斥锁。
在C++中:
class Manager
{
public:
    static Manager* instance();
    static void destroy();
}

在Rust中:
use std::{mem};

struct Manager {
    count: i32
}

static mut MANAGER_INSTANCE: Option<&'static mut Manager> = Option::None;

impl Drop for Manager {
    fn drop(&mut self) {
        println!("Dropping me!");
    }
}

impl Manager {
    pub fn instance() -> &'static mut Manager {
        unsafe {
            match MANAGER_INSTANCE {
                Option::Some(ref mut manager) => *manager,
                Option::None => {
                    println!("new instance!");
                    let manager_box = Box::new(Manager {count: 0});
                    let manager_raw = Box::into_raw(manager_box);
                    MANAGER_INSTANCE = Some(&mut *manager_raw);
                    &mut *manager_raw
                }
            }
        }
    }

    pub fn destroy() {
        unsafe {
            if let Some(raw) = mem::replace(&mut MANAGER_INSTANCE, None) {
                Box::from_raw(raw);
            }
        }
    }

    pub fn call_me(&mut self) {
        self.count += 1;
        println!("count: {}", self.count);
    }
}



#[tokio::main]
pub async fn main() {
    Manager::instance().call_me();
    Manager::instance().call_me();
    Manager::instance().call_me();
    Manager::destroy();
    Manager::instance().call_me();
    Manager::instance().call_me();
    Manager::instance().call_me();
    Manager::destroy();

    tokio::spawn(async {
        Manager::destroy();
        Manager::instance().call_me();
        Manager::destroy();
    }).await;
}

为什么不直接使用OnceCell<Mutex>或者thread_local!(RefCell)呢?你的代码既不安全又不可靠。 - undefined

-3

我的有限解决方案是定义一个结构体而不是全局可变的变量。为了使用该结构体,外部代码需要调用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();
  ...
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接