克隆Arc时会发生什么?

时间:2016-12-05 23:02:18

标签: concurrency rust clone

我正在学习并发,并希望澄清我对以下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是自有值。这里有一个变量阴影。

3 个答案:

答案 0 :(得分:29)

  

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

Arc代表 A tomically R eference C Arc管理一个对象(类型为T)并充当代理以允许共享所有权,这意味着:一个对象归所有者所有多个名字。哇,这听起来很抽象,让我们分解吧!

共享所有权

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

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

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

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以原子方式递增和递减其计数器。这意味着多个线程可以同时递增和递减计数器而不会出现问题。这就是为什么你可以跨越线程边界发送Arc,而不是Rc s。

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

结论

  

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

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

答案 1 :(得分:4)

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

首先,在Rust中要记住的一件重要事情是,如果你知道你在做什么,它实际上可以超出编译器提供的“安全范围”。因此,尝试推断一些标准库类型如何在内部工作,以所有权系统作为理解基础可能没有多大意义。

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

所以在高级别上,是的,clone()返回一个新的Arc实例,并且该新实例的所有权将移动到赋值的左侧。但是,在新的Arc实例内部仍然指向旧实例的位置..通过原始指针(或者它出现在源中,通过Shared实例,它是原始指针的包装器)。原始指针周围的包装器是我想象的文档称为“拥有的句柄”。

答案 2 :(得分:4)

std::sync::Arc智能指针,可添加以下功能:

  

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

clone(及其非线程安全的朋友std::rc::Rc)允许共享所有权。这意味着多个&#34;处理&#34;指向相同的值。无论何时克隆句柄,引用计数器都会递增。每当句柄被丢弃时,计数器就会递减。当计数器变为零时,释放句柄指向的值。

请注意,此智能指针调用数据的基础clone方法;事实上,可能不需要成为潜在的Arc方法! clone处理调用&Foo时发生的情况。

  

什么是新的&#34;拥有的句柄&#34;?这听起来像是对数据的引用?

不是引用。在更广泛的编程和英语意义上的&#34;引用&#34;,是一个参考。在Rust引用(Mutex)的特定意义上,不是引用。令人困惑,对吧?

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

  

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

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

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