我正在学习并发,并希望澄清我对以下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
是自有值。这里有一个变量阴影。
答案 0 :(得分:29)
[...]
let data = data.clone()
上发生了什么?
Arc
代表 A tomically R eference C 。 Arc
管理一个对象(类型为T
)并充当代理以允许共享所有权,这意味着:一个对象归所有者所有多个名字。哇,这听起来很抽象,让我们分解吧!
假设您有一个为您的家人购买的Turtle
类型的对象。现在出现的问题是你无法指定一只明确的乌龟主人:每个家庭成员都拥有那只宠物!这意味着(并且抱歉在这里病态),如果一个家庭成员死亡,乌龟将不会与该家庭成员一起死亡。只有家里的所有成员都离开了,乌龟才会死去。 每个人都拥有,最后一个人清理。
那么你如何在Rust中表达这种共享所有权?您很快就会注意到,只使用标准方法是不可能的:您总是必须选择一个所有者,而其他人只会引用乌龟。不好!</ p>
所以来Rc
和Arc
(为了这个故事,它的用途完全相同)。这些允许通过修补不安全的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
甚至尝试修改状态。
这比其他语言更精细,但允许以新颖的方式重用这些部分。