如何在 Rust 中创建一个安全的静态单例?

40
这是一个有争议的话题,所以让我先解释一下我的使用情况,然后再谈论实际问题。
我发现对于一些不安全的事情来说,确保不泄漏内存非常重要;如果你开始使用transmute()和forget(),这实际上是相当容易做到的。例如,将一个盒装实例传递给C代码进行任意时间的操作,然后通过使用transmute将其取回并“复活”它。
想象一下,我有一个针对这种API的安全包装器:
trait Foo {}
struct CBox;

impl CBox {
    /// Stores value in a bound C api, forget(value)
    fn set<T: Foo>(value: T) {
        // ...
    }

    /// Periodically call this and maybe get a callback invoked
    fn poll(_: Box<Fn<(EventType, Foo), ()> + Send>) {
        // ...
    }
}

impl Drop for CBox {
    fn drop(&mut self) {
        // Safely load all saved Foo's here and discard them, preventing memory leaks
    }
}

为了测试这段代码是否真的没有内存泄漏问题,我需要进行类似以下的测试:

#[cfg(test)]
mod test {

    struct IsFoo;
    impl Foo for IsFoo {}
    impl Drop for IsFoo {
        fn drop(&mut self) {
            Static::touch();
        }
    }

    #[test]
    fn test_drops_actually_work() {
        guard = Static::lock(); // Prevent any other use of Static concurrently
        Static::reset(); // Set to zero
        {
            let c = CBox;
            c.set(IsFoo);
            c.set(IsFoo);
            c.poll(/*...*/);
        }
        assert!(Static::get() == 2); // Assert that all expected drops were invoked
        guard.release();
    }
}

您如何创建这种类型的静态单例对象?

它必须使用Semaphore样式的守卫锁来确保多个测试不会同时运行,然后不安全地访问某种静态可变值。

我认为也许这个实现可以工作,但实际上它失败了,因为偶尔会出现竞争条件导致init的重复执行:

/// Global instance
static mut INSTANCE_LOCK: bool = false;
static mut INSTANCE: *mut StaticUtils = 0 as *mut StaticUtils;
static mut WRITE_LOCK: *mut Semaphore = 0 as *mut Semaphore;
static mut LOCK: *mut Semaphore = 0 as *mut Semaphore;

/// Generate instances if they don't exist
unsafe fn init() {
    if !INSTANCE_LOCK {
        INSTANCE_LOCK = true;
        INSTANCE = transmute(box StaticUtils::new());
        WRITE_LOCK = transmute(box Semaphore::new(1));
        LOCK = transmute(box Semaphore::new(1));
    }
}

请注意,与普通程序不同的是,在 Rust 中,您不能确定您的入口点(main)始终在单个任务中运行,Rust 中的测试运行器没有像这样的任何单个入口点。
显然,除了指定任务的最大数量之外,只有少数测试需要执行此类操作;对于这种情况,将测试任务池限制为一个是缓慢且无意义的。
5个回答

41

看起来这是使用std::sync::Once的案例:

use std::sync::{Once, ONCE_INIT};
static INIT: Once = ONCE_INIT;

然后在您的测试中调用

INIT.doit(|| unsafe { init(); });

Once保证你的init只会被执行一次,无论你调用INIT.doit()多少次。


没有 INIT.doit(..)。我尝试使用 INIT.call_once(..) 代替,但是发现另一个错误,即 INIT 被“污染”了。 - BingLi224

26

另请参见lazy_static,它使事情更加符合人体工程学。对于每个变量,它做的基本上与静态Once相同,但将其包装在实现了Deref的类型中,以便您可以像访问正常引用一样访问它。

使用方法如下(来自文档):

#[macro_use]
extern crate lazy_static;

use std::collections::HashMap;

lazy_static! {
    static ref HASHMAP: HashMap<u32, &'static str> = {
        let mut m = HashMap::new();
        m.insert(0, "foo");
        m.insert(1, "bar");
        m.insert(2, "baz");
        m
    };
    static ref COUNT: usize = HASHMAP.len();
    static ref NUMBER: u32 = times_two(21);
}

fn times_two(n: u32) -> u32 { n * 2 }

fn main() {
    println!("The map has {} entries.", *COUNT);
    println!("The entry for `0` is \"{}\".", HASHMAP.get(&0).unwrap());
    println!("A expensive calculation on a static results in: {}.", *NUMBER);
}

请注意,自动引用意味着每当您在静态变量上调用方法时,甚至不需要使用*。该变量将在第一次进行Deref操作时初始化。

但是,lazy_static 变量是不可变的(因为它们位于引用之后)。如果您想要一个可变的静态变量,则需要使用 Mutex

lazy_static! {
    static ref VALUE: Mutex<u64>;
}

impl Drop for IsFoo {
    fn drop(&mut self) {
        let mut value = VALUE.lock().unwrap();
        *value += 1;
    }
}

#[test]
fn test_drops_actually_work() {
    // Have to drop the mutex guard to unlock, so we put it in its own scope
    {
        *VALUE.lock().unwrap() = 0;
    }
    {
        let c = CBox;
        c.set(IsFoo);
        c.set(IsFoo);
        c.poll(/*...*/);
    }
    assert!(*VALUE.lock().unwrap() == 2); // Assert that all expected drops were invoked
}

3
如果你想要变异,那就是这个问题如何创建一个全局的、可变的单例? 的关键所在。 - Shepmaster
我刚刚看到一个类似的“单例”需求,最终使用lazy_static找到了一个很好的解决方案(至少对于我的情况而言——顺便说一句,我正在学习Rust),基本上就像这里描述的那样:https://users.rust-lang.org/t/ownership-issue-with-a-static-hashmap/27239/10?u=carueda - carueda

4
如果你愿意使用 Rust 的夜版(nightly Rust),你可以使用 SyncLazy 替代外部的 lazy_static crate。
#![feature(once_cell)]

use std::collections::HashMap;

use std::lazy::SyncLazy;

static HASHMAP: SyncLazy<HashMap<i32, String>> = SyncLazy::new(|| {
    println!("initializing");
    let mut m = HashMap::new();
    m.insert(13, "Spica".to_string());
    m.insert(74, "Hoyten".to_string());
    m
});

fn main() {
    println!("ready");
    std::thread::spawn(|| {
        println!("{:?}", HASHMAP.get(&13));
    }).join().unwrap();
    println!("{:?}", HASHMAP.get(&74));

    // Prints:
    //   ready
    //   initializing
    //   Some("Spica")
    //   Some("Hoyten")
}

3

虽然LazyLock仍然只能在夜间使用,但OnceLock已经稳定下来(自Rust 1.70版本以来)。以下是一个示例,展示了它的使用方法:

use std::sync::OnceLock;

static SOME_URL: OnceLock<String> = OnceLock::new();

fn read_url_from_somewhere() -> String {
    // This function will only be called once.
    // Imagine we get this URL from a file or another service.
    format!("https://a-funny-url-here.com")
}

fn get_url() -> &'static str {
    SOME_URL.get_or_init(|| {
        read_url_from_somewhere()
    })    
}

fn main() {
    println!("The URL from the main thread: {}", get_url());
    std::thread::spawn(|| {
        println!("Same URL from another thread: {}", get_url());
    }).join().unwrap();
}

输出:

The URL from the main thread: https://a-funny-url-here.com
Same URL from another thread: https://a-funny-url-here.com

0
使用标准库中的thread_local!(),具有单例的可变性,而无需使用不安全的代码或Mutex包装。
对于不需要被释放的单例类型,您不需要跟踪任何额外的状态,因为它们会在声明它们的线程结束时被释放(通常是主线程)。
use std::cell::RefCell;

/// Global instance
thread_local! {
    static INSTANCE_LOCK: RefCell<bool> = RefCell::new(false);
    static INSTANCE: RefCell<StaticUtils> = RevCell::new(StaticUtils::new());
    static WRITE_LOCK: RefCell<Semaphore> = RevCell::new(Semaphore::new(1));
    static LOCK: RefCell<Semaphore> = RevCell::new(Semaphore::new(1));
}

/// Accessing the singletons
INSTANCE_LOCK.with(|lock| { lock.borrow() ‹read the lock› });
INSTANCE.with(|utils| { utils.borrow() ‹read the utils› });
WRITE_LOCK.with(|sem| { sem.borrow() ‹read the semaphore› });
LOCK.with(|lock| { lock.borrow() ‹read the lock› });

/// Accessing the singletons mutually
INSTANCE_LOCK.with(|lock| { lock.borrow_mut() ‹change the lock› });
INSTANCE.with(|utils| { utils.borrow_mut() ‹change the utils› });
WRITE_LOCK.with(|sem| { sem.borrow_mut() ‹change the semaphore› });
LOCK.with(|lock| { lock.borrow_mut() ‹change the lock› });

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