首页 > 解决方案 > 可以在安全的 Rust 中实现没有可变性的单线程零成本记忆吗?

问题描述

我有兴趣找到或实现一个 Rust 数据结构,它提供了一种零成本的方式来记忆具有任意输出类型的单个计算T。具体来说,我想要一个泛型类型Cache<T>,其内部数据占用的空间不超过Option<T>,具有以下基本 API:

impl<T> Cache<T> {
    /// Return a new Cache with no value stored in it yet.
    pub fn new() -> Self {
        // ...
    }

    /// If the cache has a value stored in it, return a reference to the 
    /// stored value. Otherwise, compute `f()`, store its output
    /// in the cache, and then return a reference to the stored value.
    pub fn call<F: FnOnce() -> T>(&self, f: F) -> &T {
        // ...
    }
}

这里的目标是能够Cache在单个线程中共享对 a 的多个不可变引用,并且此类引用的任何持有者都能够访问该值(如果是第一次则触发计算)。由于我们只需要能够Cache在单个线程中共享 a,因此它没有必要是Sync.

这是一种在底层使用安全(或者至少我认为它是安全的)实现 API 的方法unsafe

use std::cell::UnsafeCell;

pub struct Cache<T> {
    value: UnsafeCell<Option<T>>
}

impl<T> Cache<T> {
    pub fn new() -> Self {
        Cache { value: UnsafeCell::new(None) }
    }

    pub fn call<F: FnOnce() -> T>(&self, f: F) -> &T {
        let ptr = self.value.get();
        unsafe {
            if (*ptr).is_none() {
                let t = f();
                // Since `f` potentially could have invoked `call` on this
                // same cache, to be safe we must check again that *ptr
                // is still None, before setting the value.
                if (*ptr).is_none() {
                    *ptr = Some(t);
                }
            }
            (*ptr).as_ref().unwrap()
        }
    }
}

是否可以在安全的 Rust 中实现这样的类型(即不编写我们自己的unsafe代码,只是间接依赖unsafe标准库中的代码)?

显然,该call方法需要 mutating self,这意味着Cache必须使用某种形式的内部可变性。但是,它似乎无法使用 a Cell,因为Cell无法按照上述所需 API 的要求来检索对封闭值的引用call。这是有充分理由的,因为Cell提供这样的引用是不合理的,因为它无法确保引用的值在引用的生命周期内不会发生变化。另一方面,对于在被调用一次Cache之后的类型,call上面的 API 并没有提供任何方法让存储的值再次发生变异,因此它可以安全地分发一个生命周期可能很长的引用就像它Cache本身一样。

如果Cell不能工作,我很好奇 Rust 标准库是否可以为内部可变性提供一些其他安全的构建块,可以用来实现 this Cache

既不RefCell也不Mutex实现这里的目标:

  1. 它们不是零成本:它们涉及存储比 an 更多的数据Option<T>并添加不必要的运行时检查。
  2. 它们似乎没有提供任何方法来返回具有我们想要的生命周期的真实引用——相反,我们只能返回 a Refor MutexGuardwhich 不是同一件事。

仅使用 anOption不会提供相同的功能:如果我们共享对 的不可变引用,则Cache此类引用的任何持有者都可以调用call并获得所需的值(并Cache在过程中改变 ,以便将来的调用将检索相同的值);然而,共享对 a 的不可变引用Option,不可能改变Option,所以它不能工作。

标签: rust

解决方案


推荐阅读