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


140

在系统中只有一个实例的情况下,创建和使用结构的最佳方法是什么?是的,这是必需的,它是OpenGL子系统,将其制作多个副本并将其传递到任何地方都会增加混乱,而不是缓解它。

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


1
您是否看过现有的OpenGL Rust绑定如何处理这个问题?
Shepmaster 2015年

20
是的,这是必需的,它是OpenGL子系统,将其制作多个副本并将其传递到任何地方都会增加混乱,而不是缓解它。=>这不是必需的定义,也许很方便(起初),但不是必需的。
Matthieu M.

3
是的,你有一点。尽管由于OpenGL毕竟是一个大型状态机,但我可以肯定的是,在任何地方都不会复制它,使用它只会导致OpenGL错误。
stevenkucera 2015年

Answers:


197

非回答答案

通常避免使用全局状态。取而代之的是,在较早的某个位置(可能在中main)构造对象,然后将对该对象的可变引用传递到需要它的位置。通常,这将使您的代码更容易推理,并且不需要太多的向后弯曲。

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

还想做一个...吗?

使用延迟静态

慵懒的静电箱可以带走一些手动创建一个单独的苦差事。这是一个全局可变向量:

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

如果删除,Mutex那么您将拥有一个没有任何可变性的全局单例。

您也可以使用a RwLock代替a Mutex来允许多个并发阅读器。

使用one_cell

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

如果删除,Mutex那么您将拥有一个没有任何可变性的全局单例。

您也可以使用a RwLock代替a Mutex来允许多个并发阅读器。

特例:原子

如果只需要跟踪整数值,则可以直接使用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极大地削弱了,对现代Rust 进行了一些调整。您还应该看看的现代实现io::Lazy。我已对每一行的内容进行了内联注释。

use std::sync::{Arc, Mutex, Once};
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::new();

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

打印输出:

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.42.0进行编译。实际的实现Stdin使用一些不稳定的功能来尝试释放已分配的内存,而此代码并未这样做。

确实,您可能想要制作SingletonReader工具DerefDerefMut因此您不必戳入对象并自己将其锁定。

所有这些工作都是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

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


72
经过深思熟虑,我坚信不要使用Singleton,而是完全不使用全局变量并传递所有内容。由于很清楚哪些函数可以访问渲染器,因此使代码更具自记录性。如果我想改回单身人士,这样做会比其他方式容易。
stevenkucera 2015年

4
感谢您的回答,它很有帮助。我只是想在这里发表评论,以描述我认为是lazy_static!的有效用例。我使用它来连接到允许加载/卸载模块(共享对象)的C应用程序,而rust代码就是这些模块之一。除了使用全局加载时,我没有其他选择,因为我根本无法控制main()以及核心应用程序如何与我的模块交互。基本上,我需要一个可以在我的mod加载后在运行时添加的事物向量。
Moises Silva

1
@MoisesSilva总是出于某种原因需要单例,但是在很多情况下并不需要使用它。在不知道您的代码的情况下,C应用程序可能应该允许每个模块返回“用户数据” void *,然后将其传递回每个模块的方法中。这是C代码的典型扩展模式。如果应用程序不允许这样做,并且您无法更改它,那么可以,单例可能是一个很好的解决方案。
Shepmaster '16

3
@Worik您是否愿意解释原因?我不鼓励人们使用大多数语言来做一个不好的主意(甚至OP都认为全局是对其应用的错误选择)。那就是一般的意思。然后,我将为您展示两种解决方案。我只是lazy_static在Rust 1.24.1中测试了示例,它确实可以正常工作。这里什么external static都没有。也许您需要检查一下事情以确保您完全理解答案。
Shepmaster

1
@Worik如果您需要有关如何使用板条箱的基础知识方面的帮助,建议您重新阅读The Rust Programming Language关于创建猜谜游戏章节介绍了如何添加依赖项。
Shepmaster

0

使用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

回答我自己重复的问题

Cargo.toml:

[dependencies]
lazy_static = "1.4.0"

板条箱根(lib.rs):

#[macro_use]
extern crate lazy_static;

初始化(不需要不安全的块):

/// EMPTY_ATTACK_TABLE defines an empty attack table, useful for initializing attack tables
pub const EMPTY_ATTACK_TABLE: AttackTable = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

lazy_static! {
    /// KNIGHT_ATTACK is the attack table of knight
    pub static ref KNIGHT_ATTACK: AttackTable = {
        let mut at = EMPTY_ATTACK_TABLE;
        for sq in 0..BOARD_AREA{
            at[sq] = jump_attack(sq, &KNIGHT_DELTAS, 0);
        }
        at
    };
    ...

编辑:

设法通过一次宏来解决它,它不需要宏。

Cargo.toml:

[dependencies]
once_cell = "1.3.1"

square.rs:

use once_cell::sync::Lazy;

...

/// AttackTable type records an attack bitboard for every square of a chess board
pub type AttackTable = [Bitboard; BOARD_AREA];

/// EMPTY_ATTACK_TABLE defines an empty attack table, useful for initializing attack tables
pub const EMPTY_ATTACK_TABLE: AttackTable = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

/// KNIGHT_ATTACK is the attack table of knight
pub static KNIGHT_ATTACK: Lazy<AttackTable> = Lazy::new(|| {
    let mut at = EMPTY_ATTACK_TABLE;
    for sq in 0..BOARD_AREA {
        at[sq] = jump_attack(sq, &KNIGHT_DELTAS, 0);
    }
    at
});

2
与已有的答案lazy_static和较新的答案相比,该答案没有提供任何新内容once_cell。在SO上将事物标记为重复项的目的是避免具有冗余信息。
Shepmaster
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.