安全地返回对内部节点的多个引用,同时仍然允许其他节点的突变

Posted

技术标签:

【中文标题】安全地返回对内部节点的多个引用,同时仍然允许其他节点的突变【英文标题】:Safely return multiple references to internal nodes, while still allowing mutation of other nodes 【发布时间】:2019-06-03 22:50:28 【问题描述】:

例如,假设我有一个不允许删除节点的链表。

是否可以返回对已插入值的共享引用,同时仍允许更改节点的相对顺序或插入新节点?

只要一次只使用一个节点对列表进行变异,即使通过其中一个节点的变异也应该是安全的“在纸上”。是否可以在 rust 的所有权系统中表示这一点?

我特别感兴趣的是这样做没有运行时开销(可能在实现中使用 unsafe,但不在接口中)。

编辑:根据要求,这是一个示例,概述了我的想法。

let list = MyLinkedList::<i32>::new()
let handle1 = list.insert(1); // Returns a handle to the inserted element.
let handle2 = list.insert(2);

let value1 : &i32 = handle1.get();
let value2 : &i32 = handle2.prev().get(); // Ok to have two immutable references to the same element.
list.insert(3); // Also ok to insert and swap nodes, while the references are held.
list.swap(handle1,handl2);
foo(value1,value2);

let exclusive_value: &mut i32 = handle1.get_mut(); // While this reference is held, no other handles can be used, but insertion and permutation are ok 
handle5 = list.insert(4);
list.swap(handle1, handle2);

换句话说,列表的节点内包含的数据被视为一种可以共享/可变借用的资源,节点之间的链接是另一种可以共享/可变借用的资源。

【问题讨论】:

如果你想同时拥有多个“游标”,并且每个游标都应该能够改变列表,你需要某种锁来防止这些改变同时发生。在单线程情况下,这可以使用RefCell 来实现,而在多线程情况下,可以使用MutexRwLock。无论如何,如果要确保内存安全,将会有一些运行时“开销”。 (引号中的开销,因为我不确定是否可以将实现目标所需的东西称为开销。) 我阅读了您的描述,但我仍然不清楚您真正想要什么。这就是为什么最好提供一个代码示例,即使它只是“这里是一个接口和使用示例”。 (1) 节点应该拥有结构(共享)还是仅在结构中引用节点? (2) 是否所有节点都是无所不能的,或者在任何时间点都可以有多达 N 个“视图”节点但只有一个“修改器”节点? (3) “允许改变节点的相对顺序”是指移动节点,还是仅仅更新链接它们的指针? (4) 与Peter的问题相关:可以从任意节点观察相邻节点,还是只观察节点持有的元素。 【参考方案1】:

换句话说,列表的节点内包含的数据被视为一种可以共享/可变借用的资源,节点之间的链接是另一种可以共享/可变借用的资源。

处理这种空间分区的想法是为每个分区引入不同的“键”;这很容易,因为它们是静态的。这被称为 PassKey 模式。

在没有 brands 的情况下,它仍然需要运行时检查:为了安全起见,必须验证 elements-key 是否绑定到此特定列表实例。但是,这是一个始终为true 的只读比较,因此就运行时检查而言,性能几乎是一样的。

简单地说:

let (handles, elements) = list.keys();
let h0 = handles.create(4);
handles.swap(h0, h1);
let e = elements.get(h0);

在您的用例中:

始终可以更改链接,因此我们将为此使用内部可变性。 对句柄内元素的借用检查将通过借用elements来执行。

完整的实现可以在here找到。它大量使用unsafe,我不保证它是完全安全的,但希望它足以用于演示。


在这个实现中,我选择了哑句柄并实现了对键类型本身的操作。这限制了需要从主列表中借用的类型数量,并简化了借用。

那么核心思想:

struct LinkedList<T> 
    head: *mut Node<T>,
    tail: *mut Node<T>


struct Handles<'a, T> 
    list: ptr::NonNull<LinkedList<T>>,
    _marker: PhantomData<&'a mut LinkedList<T>>,


struct Elements<'a, T> 
    list: ptr::NonNull<LinkedList<T>>,
    _marker: PhantomData<&'a mut LinkedList<T>>,

LinkedList&lt;T&gt; 将充当存储,但只会执行 3 个操作:

建设, 破坏, 分发钥匙。

HandlesElements 这两个键都将可变地借用列表,保证单个(每个)可以同时存在。借用检查将阻止创建新的 HandlesElements 如果它们的任何实例仍然存在于此列表中:

list:授予对列表存储的访问权限; Elements 只会使用它来检查(必要的)运行时不变量,并且永远不会取消引用它。 _marker: 是借位检查真正保证排他性的关键。

到目前为止听起来很酷吗?为了完成,最后两个结构然后:

struct Handle<'a, T> 
    node: ptr::NonNull<Node<T>>,
    list: ptr::NonNull<LinkedList<T>>,
    _marker: PhantomData<&'a LinkedList<T>>,


struct Node<T> 
    data: T,
    prev: *mut Node<T>,
    next: *mut Node<T>,

Node 是有史以来最明显的双向链表表示,所以我们做对了。 Handle&lt;T&gt; 中的listElements 中的目的完全相同:验证HandleHandles/Elements 都在谈论list 的同一个实例。 get_mut 的安全至关重要,否则有助于避免错误。

Handle&lt;'a, T&gt; 终生与LinkedList 联系在一起有一个微妙的原因。我很想删除它,但是这将允许从列表创建句柄,销毁列表,然后在同一地址重新创建列表......并且handle.node 现在将悬空!

而且,我们只需要在HandlesElements 上实现我们需要的方法。几个示例:

impl<'a, T> Handles<'a, T> 
    pub fn push_front(&self, data: T) -> Handle<'a, T> 
        let list = unsafe  &mut *self.list.as_ptr() ;

        let node = Box::into_raw(Box::new(Node  data, prev: ptr::null_mut(), next: list.head ));
        unsafe  &mut *node .set_neighbours();

        list.head = node;

        if list.tail.is_null() 
            list.tail = node;
        

        Handle 
            node: unsafe  ptr::NonNull::new_unchecked(node) ,
            list: self.list, _marker: PhantomData,
        
    

    pub fn prev(&self, handle: Handle<'a, T>) -> Option<Handle<'a, T>> 
        unsafe  handle.node.as_ref() .prev().map(|node| Handle 
            node,
            list: self.list,
            _marker: PhantomData
        )
    

还有:

impl<'a, T> Elements<'a, T> 
    pub fn get<'b>(&'b self, handle: Handle<'a, T>) -> &'b T 
        assert_eq!(self.list, handle.list);

        let node = unsafe  &*handle.node.as_ptr() ;
        &node.data
    

    pub fn get_mut<'b>(&'b mut self, handle: Handle<'a, T>) -> &'b mut T 
        assert_eq!(self.list, handle.list);

        let node = unsafe  &mut *handle.node.as_ptr() ;
        &mut node.data
    

这应该是安全的,因为:

Handles,在创建新句柄后,只会访问其链接。 Elements 只返回对data 的引用,并且在访问它们时无法修改链接。

使用示例:

fn main() 
    let mut linked_list = LinkedList::default();
    
        let (handles, mut elements) = linked_list.access();
        let h0 = handles.push_front("Hello".to_string());

        assert!(handles.prev(h0).is_none());
        assert!(handles.next(h0).is_none());

        println!("", elements.get(h0));

        let h1 = 
            let first = elements.get_mut(h0);
            first.replace_range(.., "Hallo");

            let h1 = handles.push_front("World".to_string());
            assert!(handles.prev(h0).is_some());

            first.replace_range(.., "Goodbye");

            h1
        ;

        println!(" ", elements.get(h0), elements.get(h1));

        handles.swap(h0, h1);

        println!(" ", elements.get(h0), elements.get(h1));
    
    
        let (handles, elements) = linked_list.access();

        let h0 = handles.front().unwrap();
        let h1 = handles.back().unwrap();
        let h2 = handles.push_back("And thanks for the fish!".to_string());

        println!(" ! ", elements.get(h0), elements.get(h1), elements.get(h2));
    

【讨论】:

非常酷!在这种情况下,我无法在谷歌上找到任何关于“品牌”的信息。我认为它类似于引用列表特定实例的依赖类型? @JeremySalwen:是的,想法是在编译时将两个变量链接在一起(例如,一个列表和一个保证对其有效的索引),然后你只能编写函数接受具有相同“品牌”的两个变量。在 Rust 中,生命周期非常接近,但是允许编译器合并生命周期区域,因此您不能保证两个列表实例具有不同的生命周期,因此它们不能用作品牌。见reddit.com/r/rust/comments/2s2etw/…

以上是关于安全地返回对内部节点的多个引用,同时仍然允许其他节点的突变的主要内容,如果未能解决你的问题,请参考以下文章

如何防止两个操作相互交错,同时仍然允许并发执行?

为什么要安全地发布对象,为什么需要“存储对最终字段的引用”和“正确构造的对象”?

为啥我在函数内部使用引用并通过引用返回它仍然有效? [复制]

面试官:如何安全地使用List

防火墙安全小知识

jQuery对DOM节点进行操作(删除节点)