克隆 Arc 时会发生什么?

What happens when an Arc is cloned?

我正在学习并发,想澄清我对以下内容的理解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 书说

we use clone() to create a new owned handle. This handle is then moved into the new thread.

什么是新的"owned handle"?听起来像是对数据的引用?

既然clone取了一个&self,returns取了一个Self,那么每个线程修改的是原始数据而不是副本吗?我想这就是为什么代码在这里没有使用 data.copy() 而是 data.clone() 的原因。

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

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

首先,在 Rust 中要记住的一件重要事情是,如果您知道自己在做什么,实际上可以跳出编译器提供的 "safe bounds"。因此,试图以所有权系统作为理解基础来推断某些标准库类型的内部工作方式可能没有多大意义。

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

所以在较高的层次上,是的,clone() returns 一个新的 Arc 实例和该新实例的所有权被移动到分配的左侧。但是,在内部,新的 Arc 实例仍然指向旧实例所做的地方..通过原始指针(或者如源代码中所示,通过 Shared 实例,它是原始指针的包装).原始指针周围的包装器是我想象的文档所指的 "owned handle".

[...] what is happening on let data = data.clone()?

Arc 代表 A 原子地 R 引用 CArc 管理 一个 对象(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 书中所说的 owned handle:这种 handle 的每个所有者也都拥有底层对象。

(另见)

原子性和可变性

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

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

结论

[...] is each thread modifying the original data instead of a copy?

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

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

An atomically reference counted wrapper for shared state.

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

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

What is the new "owned handle"? It sounds like a reference to the data?

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


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

A mutual exclusion primitive useful for protecting shared data

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

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