【问题标题】:What happens when an Arc is cloned?克隆 Arc 时会发生什么?
【发布时间】:2017-04-20 11:26:01
【问题描述】:

我正在学习并发,想澄清我对以下code example from the Rust book的理解。如果我错了,请纠正我。

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));

    for i in 0..3 {
        let data = data.clone();
        thread::spawn(move || {
            let mut data = data.lock().unwrap();
            data[0] += i;
        });
    }

    thread::sleep(Duration::from_millis(50));
}

let data = data.clone()线上发生了什么?

Rust 书说

我们使用clone() 创建一个新的拥有句柄。然后将此句柄移到新线程中。

什么是新的“自有句柄”?这听起来像是对数据的引用?

既然clone 接受&self 并返回Self,那么每个线程是否都在修改原始数据而不是副本?我想这就是为什么代码在这里没有使用data.copy(),而是使用data.clone()

右侧的data 是一个引用,左侧的data 是一个拥有的值。这里有一个变量阴影。

【问题讨论】:

    标签: concurrency rust clone


    【解决方案1】:

    [...]let data = data.clone() 上发生了什么?

    Arc 代表 Atomically Reference Counted。一个Arc 管理一个 对象(T 类型)并充当代理以允许共享所有权,这意味着:一个对象由多个名称拥有。哇,这听起来很抽象,让我们分解一下!

    共享所有权

    假设您有一个类型为 Turtle ? 的对象,它是您为家人购买的。现在问题出现了,您无法指定乌龟的明确所有者:每个家庭成员都拥有那只宠物!这意味着(并且很抱歉在这里病态)如果家庭中的一个成员死了,乌龟不会和那个家庭成员一起死。只有当家庭的所有成员都离开时,乌龟才会死。 每个人都拥有,最后一个清理干净

    那么您将如何在 Rust 中表达这种共享所有权?您很快就会注意到,仅使用标准方法是不可能的:您总是必须选择一个所有者,而其他所有人都只能引用海龟。不好!

    所以RcArc 来了(为了这个故事,它们的目的完全相同)。这些允许通过修改 unsafe-Rust 来实现共享所有权。让我们看看执行以下代码后的内存(注意:内存布局用于学习,可能与现实世界的内存布局不完全相同):

    let annas = Rc::new(Turtle { legs: 4 });
    

    内存:

      Stack                    Heap
      -----                    ----
    
    
      annas:
    +--------+               +------------+
    | ptr: o-|-------------->| count: 1   |
    +--------+               | data: ?   |
                             +------------+
    

    我们看到海龟生活在堆上......旁边的计数器设置为 1。这个计数器知道对象 data 当前拥有多少个所有者。 1 是正确的:annas 是目前唯一拥有乌龟的人。让我们clone()Rc 来获得更多所有者:

    let peters = annas.clone();
    let bobs = annas.clone();
    

    现在内存是这样的:

      Stack                    Heap
      -----                    ----
    
    
      annas:
    +--------+               +------------+
    | ptr: o-|-------------->| count: 3   |
    +--------+    ^          | data: ?   |
                  |          +------------+
     peters:      |
    +--------+    |
    | ptr: o-|----+
    +--------+    ^
                  |
      bobs:       |
    +--------+    |
    | ptr: o-|----+
    +--------+
    

    如您所见,乌龟仍然只存在一次。但是引用计数增加了,现在是 3,这是有道理的,因为海龟现在有三个所有者。所有这三个所有者都引用堆上的这个内存块。这就是 Rust 书中所说的拥有的句柄:这种句柄的每个所有者也都拥有底层对象。

    另见"Why is std::rc::Rc<> not Copy?"

    原子性和可变性

    你问Arc<T>Rc<T>有什么区别? Arc 以原子方式递增和递减其计数器。这意味着多个线程可以同时递增和递减计数器而不会出现问题。这就是为什么您可以跨线程边界发送Arcs,但不能发送Rcs。

    现在您注意到您无法通过Arc<T> 改变数据!如果你的?失去一条腿怎么办? Arc 并非旨在允许多个所有者同时(可能)同时进行可变访问。这就是为什么你经常看到像Arc<Mutex<T>> 这样的类型。 Mutex<T> 是一种提供内部可变性的类型,这意味着您可以从&Mutex<T> 获得&mut T!这通常会与 Rust 核心原则相冲突,但它是完全安全的,因为互斥体也管理访问:您必须请求访问对象。如果另一个线程/源当前可以访问该对象,则您必须等待。因此,在某一特定时刻,只有一个线程能够访问T

    结论

    [...] 是不是每个线程都在修改原始数据而不是副本?

    正如您希望从上面的解释中理解的那样:是的,每个线程都在修改原始数据。在Arc<T> 上的clone() 不会克隆T,而只是创建另一个拥有的句柄;反过来,它只是一个指针,其行为就像它拥有底层对象一样。

    【讨论】:

    • 超级有趣又酷的解释。我希望生锈的书以后也能这样写。
    • @Lukas 我期待阅读您的更多答案,谢谢
    • Arc> 是 Rc> 的线程安全类比,如果不总是访问海龟,Arc> 可能会派上用场可变的。这三种组合都很常见。
    【解决方案2】:

    std::sync::Arc 是一个智能指针,它增加了以下能力:

    共享状态的原子引用计数包装器。

    Arc(及其非线程安全朋友std::rc::Rc)允许共享所有权。这意味着多个“句柄”指向相同的值。每当克隆句柄时,引用计数器就会递增。每当把手放下时,计数器就会递减。当计数器变为零时,句柄指向的值被释放。

    请注意,此智能指针调用数据的底层clone 方法;实际上,可能不需要底层的clone 方法! Arc 处理调用 clone 时发生的情况。

    什么是新的“自有句柄”?听起来像是对数据的引用?

    不是参考。在“reference”这个词的更广泛的编程和英语意义上,它是一个参考。在特定意义上的 Rust 引用 (&Foo) 中,它不是引用。令人困惑,对吧?


    你问题的第二部分是关于std::sync::Mutex,描述为:

    用于保护共享数据的互斥原语

    互斥锁是多线程程序中的常用工具,并且有很好的描述 其他地方,所以我不会在这里重复。需要注意的重要一点是 Rust Mutex only 使您能够修改共享状态。由Arc 决定是否允许多个所有者访问Mutex 甚至尝试修改状态。

    这比其他语言更细粒度,但允许以新颖的方式重用这些部分。

    【讨论】:

      【解决方案3】:

      我不是标准库内部的专家,我仍在学习 Rust.. 但这是我可以看到的:(you could check the source yourself too if you wanted)。

      首先,在 Rust 中要记住的重要一点是,如果您知道自己在做什么,实际上可以跨出编译器提供的“安全界限”。因此,尝试以所有权系统为理解基础来推理某些标准库类型在内部是如何工作的可能没有多大意义。

      Arc 是在内部回避所有权系统的标准库类型之一。它本质上是自己管理一个指针,调用clone()会返回一个新的Arc,它指向与原始内存完全相同的一块内存......并增加了引用计数。

      所以在高层次上,是的,clone() 返回一个新的Arc 实例,并且该新实例的所有权移至分配的左侧。但是,在内部,新的 Arc 实例仍然指向旧的实例。通过原始指针(或在源中显示,通过 Shared 实例,它是原始指针的包装器)。原始指针周围的包装器是我想象的文档所指的“拥有的句柄”。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-05-23
        • 1970-01-01
        • 1970-01-01
        • 2018-01-01
        • 2018-12-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多