我正在try 实现一个共享状态、线程安全的缓存,在RwLock后面使用HashMap.理想情况下,我希望我的接口公开一个函数,该函数--给定键--将返回对缓存中的值的引用(如果需要,首先填充它).然而,我的中级直觉告诉我,这种方法至少会遇到以下问题:

  1. 是否有可能返回对值的引用?AFAIK,HashMap可以在需要时自由重新分配(*),因此引用的生命周期不同于HashMap,并且实际上是不确定的(就编译器而言).HashMap::entryAPI返回一个引用,但当然,它的生存时间不能超过它的封闭作用域.因此,是否有必要返回一个具体的值,从而强制我为值类型实现Copy

    (*)如果值不实现Copy,这是否仍然正确?

  2. RwLock对我来说是新的.我从Mutex开始,但RwLock看起来更合适,因为对缓存的预期访问模式(即,每个键一次写入,多次读取).然而,我不确定是否可以将读锁"升级"为读写锁.Rust文档表明,读锁将阻止写锁,这将导致我建议/希望它工作的方式出现死锁.我是否可以取消读锁定并将其替换为写锁定,或者从一开始就使用完整的Mutex更容易?

顺便说一句,获取该值的函数是异步的,所以虽然我可以使用HashMap::entryAPI,但我不能使用Entry个便利函数(如or_insert)……

不管怎样,也许我有点不知所措,但这就是我到目前为止所得到的:

// NOTE The `Key` struct contains a reference, with lifetime `'key`
pub struct Cache<'key>(RwLock<HashMap<Key<'key>, Value>>);

impl<'cache, 'key> Cache<'key> {
    pub fn new() -> Arc<Self> {
        Arc::new(Self(RwLock::new(HashMap::new())))
    }

    // NOTE The key and value for the cache is derived from the `Payload` type
    pub async fn fetch(
        &'cache mut self,
        data: &'cache Payload<'key>
    ) -> Result<&'cache Value> {
        let cache = self.0.read()?;
        let key = data.into();

        Ok(match &mut cache.entry(key) {
            // Match arm type: &Value
            // FIXME This reference outlives the scope, which is a borrow violation
            Entry::Occupied(entry) => entry.get(),

            // Dragons be here...
            Entry::Vacant(ref mut slot) => {
                // FIXME How do I create a write lock here, without deadlocking on
                // the read lock that's still in scope?

                let value = data.get_value().await?;

                // FIXME `VacantEntry<'_, Key<'_>, Value>` doesn't implement `Copy`
                slot.insert(value)
            }
        })
    }
}

推荐答案

关于你问题的第二部分(将读锁升级为写锁),我只是在获得写锁之前先删除读锁.:-(

但是,关于第一部分(在此哈希映射发生Mutations 时返回对哈希映射内容的引用),我有一个线索.

在这种情况下,引用的元素基本上是共享的,所有权不仅仅是缓存本身的问题. 例如,如果我们决定在流程中的某个点清除缓存,则不能删除仍被引用的元素. 因此,我们需要为缓存中的每个值共享所有权. 如果需要,我们需要存储Arc<Value>,而不是存储Value,以便使该值比缓存更持久.

请在下面找到针对过于简单的情况(无泛型类型,硬编码示例)实现此功能的try :将一个字符串与其字符的代码向量相关联. 串行实现依赖于RefCellRc,而并行实现依赖于它们的同步类似功能RwLockArc. 在每个DEMO中,我们可以看到,当同一个字符串用于多次读取(向量被缓存)时,获取的Slice的地址保持不变.

mod serial {
    use std::{cell::RefCell, collections::HashMap, rc::Rc};

    pub struct Cache {
        data: RefCell<HashMap<String, Rc<Vec<u32>>>>,
    }

    impl Cache {
        pub fn new() -> Self {
            Self {
                data: RefCell::new(HashMap::new()),
            }
        }

        pub fn fetch(
            &self,
            key: &str,
        ) -> Rc<Vec<u32>> {
            if let Some(v) = self.data.borrow().get(key) {
                return Rc::clone(v);
            }
            let k = key.to_owned();
            let v = Rc::new(key.chars().map(|c| c as u32).collect());
            self.data.borrow_mut().insert(k, Rc::clone(&v));
            v
        }
    }

    pub fn demo() {
        println!("~~~~ serial demo ~~~~");
        let cache = Cache::new();
        for key in ["abc", "def", "ghi"] {
            for it in 0..2 {
                let v = cache.fetch(key);
                println!("iter {}: {:?} @ {:?}", it, v, v.as_ptr());
            }
        }
    }
}

mod parallel {
    use std::{collections::HashMap, sync::Arc, sync::RwLock};

    pub struct Cache {
        data: RwLock<HashMap<String, Arc<Vec<u32>>>>,
    }

    impl Cache {
        pub fn new() -> Self {
            Self {
                data: RwLock::new(HashMap::new()),
            }
        }

        pub fn fetch(
            &self,
            key: &str,
        ) -> Arc<Vec<u32>> {
            if let Some(v) = self.data.read().unwrap().get(key) {
                return Arc::clone(v);
            }
            let k = key.to_owned();
            let v = Arc::new(key.chars().map(|c| c as u32).collect());
            self.data.write().unwrap().insert(k, Arc::clone(&v));
            v
        }
    }

    pub fn demo() {
        println!("~~~~ parallel demo ~~~~");
        let cache = Cache::new();
        std::thread::scope(|s| {
            for it in 0..2 {
                s.spawn({
                    let cache = &cache;
                    move || {
                        for key in ["abc", "def", "ghi"] {
                            std::thread::yield_now();
                            let v = cache.fetch(key);
                            println!(
                                "thread {}: {:?} @ {:?}",
                                it,
                                v,
                                v.as_ptr()
                            );
                        }
                    }
                });
            }
        });
    }
}

fn main() {
    serial::demo();
    parallel::demo();
}
/*
~~~~ serial demo ~~~~
iter 0: [97, 98, 99] @ 0x557c23797bc0
iter 1: [97, 98, 99] @ 0x557c23797bc0
iter 0: [100, 101, 102] @ 0x557c23797c00
iter 1: [100, 101, 102] @ 0x557c23797c00
iter 0: [103, 104, 105] @ 0x557c23797c40
iter 1: [103, 104, 105] @ 0x557c23797c40
~~~~ parallel demo ~~~~
thread 1: [97, 98, 99] @ 0x7ff378000cc0
thread 0: [97, 98, 99] @ 0x7ff378000cc0
thread 1: [100, 101, 102] @ 0x7ff378000d00
thread 0: [100, 101, 102] @ 0x7ff378000d00
thread 1: [103, 104, 105] @ 0x7ff378000d40
thread 0: [103, 104, 105] @ 0x7ff378000d40
*/

Rust相关问答推荐

基于对vec值的引用从该值中删除该值

如何找到一个数字在二维数组中的位置(S)?

如何在Bevy/Rapier3D中获得碰撞机的计算质量?

防止cargo test 中的竞争条件

铁 rust 中的共享对象实现特征

完全匹配包含大小写的整数范围(&Q;)

返回Result<;(),框<;dyn错误>>;工作

是否提供Bundle 在可执行文件中的warp中的静态文件?

Rust wasm 中的 Closure::new 和 Closure::wrap 有什么区别

如何在 `connect_activate()` 之外创建一个 `glib::MainContext::channel()` 并将其传入?

Nom 解析器无法消耗无效输入

try 从标准输入获取用户名和密码并删除 \r\n

将原始可变指针传递给 C FFI 后出现意外值

SDL2 没有在终端键上触发?

为什么在 macOS / iOS 上切换 WiFi 网络时 reqwest 响应会挂起?

使用 HashMap 条目时如何避免字符串键的短暂克隆?

如何存储返回 Future 的闭包列表并在 Rust 中的线程之间共享它?

将数据序列化为 struct 模型,其中两个字段的数据是根据 struct 中的其他字段计算的

有没有比多个 push_str() 调用更好的方法将字符串链接在一起?

如果参数为 Send,则返回 Future Send