如何在互斥锁下返回值的子值的引用?

35

我有一个类似于这样的结构:

pub struct MyStruct {
    data: Arc<Mutex<HashMap<i32, Vec<i32>>>>,
}

我可以轻松地获取互斥锁并查询底层的 HashMap:
let d = s.data.lock().unwrap();
let v = d.get(&1).unwrap();
println!("{:?}", v);

现在我想要创建一个方法来封装查询,所以我写了以下内容:
impl MyStruct {
    pub fn get_data_for(&self, i: &i32) -> &Vec<i32> {
        let d = self.data.lock().unwrap();
        d.get(i).unwrap()
    }
}

这段代码无法编译,因为我试图返回一个指向 Mutex 下数据的引用:

error: `d` does not live long enough
  --> <anon>:30:9
   |
30 |         d.get(i).unwrap()
   |         ^
   |
note: reference must be valid for the anonymous lifetime #1 defined on the block at 28:53...
  --> <anon>:28:54
   |
28 |     pub fn get_data_for(&self, i: &i32) -> &Vec<i32> {
   |                                                      ^
note: ...but borrowed value is only valid for the block suffix following statement 0 at 29:42
  --> <anon>:29:43
   |
29 |         let d = self.data.lock().unwrap();
   |                                           ^

我可以通过将HashMap的值包装在Arc中来修复它,但这看起来很丑(Arc中的Arc)并且使代码变得复杂:
pub struct MyStruct {
    data: Arc<Mutex<HashMap<i32, Arc<Vec<i32>>>>>,
}

什么是最佳方法来解决这个问题?是否可能创建一个方法,可以在不修改数据结构的情况下实现我的要求? 完整示例代码

4
好问题。我本来期望在MutexGuard上有一个map()方法,就像Ref::map()一样... 为什么没有呢? - Lukas Kalbertodt
1
实现MyStruct { fn with_data<F:Fn(&Vec<i32))>(f:F){...} }这样行吗?基本上只是让用户提供一个函数,在锁定时修改数据,而不是尝试返回它? - dpc.pw
1
闭包方法在 Rust 代码中常用于引用、借用等。 - dpc.pw
1
这是有点可能的。但是这段代码不太好,因为每次调用deref()时都会进行查找。我认为在安全的Rust中无法做得更好。但我很想被证明是错误的。 - Lukas Kalbertodt
1
@LukasKalbertodt 我认为你的想法是正确的,但方法不对。我认为你需要一个 struct{MutexGuard<'a>,&'a Inner},并且还需要一个 deref(_mut)map 方法。这样可以在安全的 Rust 中实现任意重映射而无需每次等待锁。 - Linear
显示剩余3条评论
5个回答

19

parking_lot crate提供了一个互斥锁的实现,比std中的更好。其中一个好处是MutexGuard :: map ,它实现了类似于owning_ref 的接口。

use std::sync::Arc;
use parking_lot::{Mutex, MutexGuard, MappedMutexGuard};
use std::collections::HashMap;

pub struct MyStruct {
    data: Arc<Mutex<HashMap<i32, Vec<i32>>>>,
}

impl MyStruct {
    pub fn get_data_for(&self, i: &i32) -> MappedMutexGuard<Vec<i32>> {
        MutexGuard::map(self.data.lock(), |d| d.get_mut(i).unwrap())
    }
}

你可以在这里的游乐场上尝试它。


2
这个答案太被低估了。谢谢,这正是我在寻找的,而且我从来没有想过在“停车场”里找它。 - user4815162342

10

这个解决方案类似于@Neikos的,但使用owning_ref来持有MutexGuard和对Vec的引用:

extern crate owning_ref;
use std::sync::Arc;
use std::sync::{Mutex,MutexGuard};
use std::collections::HashMap;
use std::vec::Vec;
use owning_ref::MutexGuardRef;

type HM = HashMap<i32, Vec<i32>>;

pub struct MyStruct {
    data: Arc<Mutex<HM>>,
}

impl MyStruct {
    pub fn new() -> MyStruct {
        let mut hm = HashMap::new();
        hm.insert(3, vec![2,3,5,7]);
        MyStruct{
            data: Arc::new(Mutex::new(hm)),
        }
    }
    pub fn get_data_for<'ret, 'me:'ret, 'c>(&'me self, i: &'c i32) -> MutexGuardRef<'ret, HM, Vec<i32>> {
        MutexGuardRef::new(self.data.lock().unwrap())
               .map(|mg| mg.get(i).unwrap())
    }
}

fn main() {
    let s: MyStruct = MyStruct::new();

    let vref = s.get_data_for(&3);

    for x in vref.iter() {
        println!("{}", x);
    }
}

这种方法的优点是,可以通过owning_ref上的map方法轻松地获得指向Mutex中任何其他可达对象(例如Vec中的单个项目等)的类似引用,而无需重新实现返回类型。


这个库有一个叫做MutexGuardRef的类型,为什么不使用它呢?http://kimundi.github.io/owning-ref-rs/owning_ref/type.MutexGuardRef.html - Neikos
谢谢,我没有注意到MutexGuardRef - Chris Emerson
2
@ChrisEmerson 是否有可能修改get_data_for方法,使其不会解包get函数的结果并返回Option?我尝试自己做,但无法绕过“冲突生命周期要求”的错误。 - Sergey

8
使用实现了Deref并拥有MutexGuard的次要结构体可以实现这一点。
示例:
use std::sync::{Arc, Mutex, MutexGuard};
use std::collections::HashMap;
use std::ops::Deref;

pub struct Inner<'a>(MutexGuard<'a, HashMap<i32, Vec<i32>>>, i32);

impl<'a> Deref for Inner<'a> {
    type Target = Vec<i32>;
    fn deref(&self) -> &Self::Target {
        self.0.get(&self.1).unwrap()
    }
}
pub struct MyStruct {
    data: Arc<Mutex<HashMap<i32, Vec<i32>>>>,
}

impl MyStruct {
    pub fn get_data_for<'a>(&'a self, i: i32) -> Inner<'a> {
        let d = self.data.lock().unwrap();
        Inner(d, i)
    }
}

fn main() {
    let mut hm = HashMap::new();
    hm.insert(1, vec![1,2,3]);
    let s = MyStruct {
        data: Arc::new(Mutex::new(hm))
    };

    {
        let v = s.get_data_for(1);
        println!("{:?}", *v);
        let x : Vec<_> = v.iter().map(|x| x * 2).collect();
        println!("{:?}", x); // Just an example to see that it works
    }
}

1
感谢您的回答!我决定将Chris的答案标记为已接受,因为owning_ref不需要创建中间类型。但更重要的是,正如Lukas在评论中提到的类似建议,这将在每个deref()上调用map()闭包。另一方面,owning_ref仅调用一次查找(至少在我测试过的情况下)。 - Sergey Mitskevich

4
这是评论中提到的闭包传递方法的实现示例:
impl MyStruct {
    pub fn with_data_for<T>(&self, i: &i32, f: impl FnOnce(&Vec<i32>) -> T) -> Option<T> {
        let map_guard = &self.data.lock().ok()?;
        let vec = &map_guard.get(i)?;
        Some(f(vec))
    }
}
Rust Playground 是一个 Rust 语言在线编译器,可用于展示和分享代码。以下是使用示例:
s.with_data_for(&1, |v| {
    println!("{:?}", v);
});

let sum: i32 = s.with_data_for(&1, |v| v.iter().sum()).unwrap();
println!("{}", sum);

4
如同在“为什么我无法在同一个结构体中存储值和其引用?”中所述,Rental crate允许在某些情况下使用自引用结构体。在这里,我们将ArcMutexGuard和值捆绑成一个结构体,并通过Deref指向该值。
#[macro_use]
extern crate rental;

use std::{
    collections::HashMap, sync::{Arc, Mutex},
};

use owning_mutex_guard_value::OwningMutexGuardValue;

pub struct MyStruct {
    data: Arc<Mutex<HashMap<i32, Vec<i32>>>>,
}

impl MyStruct {
    pub fn get_data_for(&self, i: &i32) -> OwningMutexGuardValue<HashMap<i32, Vec<i32>>, Vec<i32>> {
        OwningMutexGuardValue::new(
            self.data.clone(),
            |d| Box::new(d.lock().unwrap()),
            |g, _| g.get(i).unwrap(),
        )
    }
}

rental! {
    mod owning_mutex_guard_value {
        use std::sync::{Arc, Mutex, MutexGuard};

        #[rental(deref_suffix)]
        pub struct OwningMutexGuardValue<T, U>
        where
            T: 'static,
            U: 'static,
        {
            lock: Arc<Mutex<T>>,
            guard: Box<MutexGuard<'lock, T>>,
            value: &'guard U,
        }
    }
}

fn main() {
    let mut data = HashMap::new();
    data.insert(1, vec![1, 2, 3]);
    let s = MyStruct {
        data: Arc::new(Mutex::new(data)),
    };

    let locked_data = s.get_data_for(&1);
    let total: i32 = locked_data.iter().map(|x| x * 2).sum();
    println!("{}", total);

    assert!(s.data.try_lock().is_err());

    drop(locked_data);

    assert!(s.data.try_lock().is_ok());
}

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