克隆Arc时会发生什么?

ena*_*naJ 21 concurrency clone rust

我正在学习并发性,并希望澄清我对Rust书中以下代码示例的理解.如果我错了,请纠正我.

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));
}
Run Code Online (Sandbox Code Playgroud)

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

Rust书说

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

什么是新的"拥有的手柄"?这听起来像是对数据的引用?

由于clonea &self和a 返回一个Self,是每个线程修改原始数据而不是副本?我猜这就是为什么代码没有使用data.copy()data.clone()在这里.

data右侧是一个参考,而data左边是一个拥有价值.这里有一个变量阴影.

Luk*_*odt 32

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

Arc代表 tomically ř eference Ç ounted.一个Arc管理一个(的类型的对象T),并充当代理,以允许共享所有权,这意味着:一个目的是通过多个名称拥有.哇,这听起来很抽象,让我们分解吧!

共享所有权

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

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

于是一起去RcArc(对于这个故事的缘故,服务完全相同的目的).这些允许通过修补不安全的Rust来共享所有权.让我们在执行以下代码后查看内存(注意:内存布局用于学习,可能不代表与现实世界完全相同的内存布局):

let annas = Rc::new(Turtle { legs: 4 });
Run Code Online (Sandbox Code Playgroud)

记忆:

  Stack                    Heap
  -----                    ----


  annas:
+--------+               +------------+
| ptr: o-|-------------->| count: 1   |
+--------+               | data:    |
                         +------------+
Run Code Online (Sandbox Code Playgroud)

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

let peters = annas.clone();
let bobs = annas.clone();
Run Code Online (Sandbox Code Playgroud)

现在内存看起来像这样:

  Stack                    Heap
  -----                    ----


  annas:
+--------+               +------------+
| ptr: o-|-------------->| count: 3   |
+--------+    ^          | data:    |
              |          +------------+
 peters:      |
+--------+    |
| ptr: o-|----+
+--------+    ^
              |
  bobs:       |
+--------+    |
| ptr: o-|----+
+--------+
Run Code Online (Sandbox Code Playgroud)

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

(另见"为什么std::rc::Rc<>不复制?")

原子性和可变性

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

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

结论

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

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

  • Arc <Mutex <T >>是Rc <RefCell <T >>的线程安全类比,如果对龟的访问并不总是可变的,Arc <RwLock <T >>可能会派上用场.这三种成分都很常见. (4认同)
  • 超级有趣和酷的解释.我希望将来以同样的方式写出锈书. (3认同)

Sim*_*ead 5

我不是标准库内部专家,我仍在学习Rust ..,但是我看到的是:(如果需要,您也可以自己检查源代码)。

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

Arc是在内部避开所有权系统的标准库类型之一。它本质上完全管理一个指针,并且调用clone()返回一个新指针,该指针指向Arc原始指针所处的完全相同的内存。

因此,在较高的级别上,是的,clone()返回一个新Arc实例,并且该新实例的所有权移到了分配的左侧。但是,在内部,新Arc实例仍通过原始指针(或在源中,通过Shared实例,它是原始指针的包装)指向旧实例的位置。我认为原始指针周围的包装器是文档中称为“拥有的手柄”的东西。


She*_*ter 5

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

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

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

请注意,这个智能指针并没有调用底层clone数据的方法; 实际上,可能不需要底层clone方法!Arc处理什么时候clone被调用。

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

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


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

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

互斥锁是多线程程序中的常用工具,并且在其他地方进行了详细介绍,因此在这里我不再赘述。需要注意的重要一点是,Rust Mutex 使您能够修改共享状态。Arc允许多个所有者访问Mutex甚至尝试修改状态都是取决于。

与其他语言相比,这有点粒度,但是允许这些片段以新颖的方式重复使用。