问:Rust 没有shared_ptr<T>
怎么办
答:使用 Rust 的 Arc<Mutex<T>>
存储共享的资源。
认识 Arc<T>
与 Mutex<T>
Arc<T>
相当于 C++ 的 shared_ptr<const T>
,都是带引用计数的智能指针,只是 T
不能被修改。在 Rust,为了能让它里面的东西能够被修改,那就要再给里面的东西套一层 Mutex<T>
,也就是互斥体容器,用于多线程访问里面的东西的时候上锁来防止访问冲突。最终套成了 Arc<Mutex<T>>
。
如何利用 Arc<Mutex<T>>
来修改 T
呢?
这里要用到 Mutex<T>
的 lock()
函数,也就是上锁。它返回一个 Result<MutexGuard<T>>
。这个 MutexGuard<T>
包含了你要的 T
。它会在被释放的时候自动解锁 Mutex<T>
。而此时你想要访问数据 T
只需要调用 MutexGuard<T>::DerefMut()
,它就能给你返回一个 &mut T
,这样你就能修改你的 T
了。
但是,直到你完成对 T
的操作之前,请一定确保 MutexGuard<T>
在场。否则你的 Mutex<T>
就会解锁,你就用不了 T
了。一旦 Mutex<T>
解锁,其他的线程或者程序就又可以通过调用它的 lock()
来试图获取它的数据并处理。
pub fn lock_and_use<T, F>(shared_object: Arc<Mutex<T>>, mut action: F) -> Result<(), Box<dyn Error>>
where F: FnMut(&mut T) -> Result<(), Box<dyn Error>> {
let mut guard = shared_object.lock()?;
let mut my_object = guard.deref_mut();
(action)(&mut my_object)
}
// 用法:传入你的 Arc<Mutex<T>> ,和一个闭包函数。闭包函数的参数就是你要操作的那个 T,并且闭包函数可以使用你在调用这个函数时的局部变量等。
注意事项
在多线程调用 Mutex<T>::lock()
的时候,存在一种情况:某个线程取得了这个互斥锁,但是它 panic 了。此时,这个互斥锁被标记为“中毒”状态。通常还是用 .unwrap()
去直接取得 MutexGuard<T>
,失败了就 panic。即便如此,中毒状态下的互斥锁的 lock()
返回的 Err(PoisonError<T>)
依然能取得数据,只是无法保证数据正确。
注意你使用 T
的时间长短,你的 MutexGuard<T>
存活越长,你的锁就越久,别的线程就等的越久。但如果你是单线程状态的话那就无所谓了。
我在单线程程序使用 Arc<Mutex<T>>
太麻烦也太复杂了,但是我想共享一个资源,该怎么办?
请学习 Rust 的 生命周期 概念。使用引用来引用你想要共享的资源,使用泛型参数列表的 <'a>
和引用语法的 &'a
来约束生命周期。当编译器知道你的结构体和它引用的资源处于相同的生命周期的时候,就会允许你的代码编译通过,并帮你检查生命周期的长度是否有问题,有问题就会提醒你。
生命周期太复杂了,我不想学,请问该怎么办?
把你的所有资源全部放到同一个结构体里。如果某个结构体大小不明确或者带有虚表,那就用 Box<T>
包起来,带有虚表的情况下用 Box<dyn T>
。这个 Box<T>
等同于 C++ 的 unique_ptr<T>
。
赋值语句与移动语义
由于 Rust 的赋值语句 =
是移动语义的,你可以用 =
把一个 Box<T>
随意赋值给任何一个同类型变量,当然旧变量就会变得不再可读了(但如果它具有可写属性 mut
则可以重新给它赋值,赋值后就又可以用了)。C++ 要想把 unique_ptr<T>
赋值来赋值去,不能只用 =
,必须要用 std::move()
,而旧变量理论上是不应该再拿来读取的,但是 C++ 编译器允许你读取,导致容易意外读到野数据。
Rust 的这种特性可以明显减少潜在的深拷贝行为。在 C++,你把一个类直接用 =
赋值给一个变量,它可能要把这个类里面的所有内脏都分配了内存复制一下,除非你刻意去避免这一点。而 Rust 要做拷贝就必须你显式调用 clone()
,这样你就会知道你的数据何去何从,什么时候发生了复制,什么时候只是改变了它的内容的拥有者。
我有一个 Arc<T>
了,但是我还想要一个指向相同数据的 Arc<T>
,我该怎么做?
调用 Arc<T>::clone()
就行了。这会增加一个 Arc<T>
实例,并且使引用计数增加,但是它里面的 T
不会受影响。
在智能指针方面,Rust 与 C++ 的对比
C++ 经常遇到的问题是:我想设计一个函数,它接受智能指针数据,但是我应该传智能指针的引用还是应该传它的实例?有很多网文说要传实例,否则如果你传引用,会导致共享指针的引用计数异常,发生内存泄漏。
而在传递实例的过程中,C++ 隐式拷贝了一个 shared_ptr<T>
,作为参数传递给被调用者,这一过程使共享指针引用计数加一;被调用者结束后,它的参数列表里的 shared_ptr<T>
被销毁,引用计数减一,平衡了。
Rust 的做法则是既可以传递 &Arc<T>
,也可以传递 Arc<T>.clone()
,对于是否发生了复制这一块是显式的。
C++ 的 shared_ptr<T>
在访问的时候不会上锁(MSVC 除外,会在你操作它的时候偷偷上锁),而 Rust 的 Arc<Mutex<T>>
因为有 Mutex<T>
而要求强制上锁,各有利弊。多线程使用 C++ 一定要多注意数据争夺问题,确保线程安全并合理上锁;多线程使用 Rust 要权衡好到底要用 Arc<Mutex<T>>
还是生命周期机制 <'a>
。
有人跟我说,非多线程情况下,Rust 可以不用 Arc<T>
,可以用 Rc<T>
。我表示,哼。按照这个道理,那是不是可以比喻成,反正我的公司目前只有一个员工,那我的公司的厕所就只有一个坑位就够了呢?