前言
在之前22
篇文章里你接触到了Rust
的基础核心概念,你知道了在Rust
中一个数值只能有一个所有者,你还知道了生命周期的概念。这些内容都稍微有点独特和奇怪,但到这里我相信你已经开始学会接受了
不过当你开始一个项目后,你可就能会陷入到引用和生命周期注释的困境中,Rust
中的Sync
、Send
、生命周期、数值借用等会让你想要放弃
而我们接下来介绍的Rc
、Arc
、Mutex
、RwLock
将会在一定程度上解救你
正文
Rc & Arc
Rc
和 Arc
是Rust
中的引用计数类型,你可以通过计数引用来管理内存
假定这样一个场景,你有一个需要在多个数据结构中指向的数值。例如你正在开发一款关于太空海盗的游戏,它拥有高科技的藏宝图,并且指向宝藏的实际位置,自始至终我们的地图都需要保持一个对宝藏的引用,对应的代码可能如下:
let booty = Treasure { dubloons: 1000 };
let my_map = TreasureMap::new(&booty);
let your_map = my_map.clone();
结构体Treasure
的内容比较直观:
#[derive(Debug)]
struct Treasure {
dubloons: u32,
}
TreasureMap
结构体保存了一个引用,此时Rust
编译器会提示需要生命周期注释,对于一个Rust
初学者来说可能会接受来自编译器的提示,不过按照编译器的提示来做是正确的么?
#[derive(Clone, Debug)]
struct TreasureMap<'a> {
treasure: &'a Treasure,
}
impl<'a> TreasureMap<'a> {
fn new(treasure: &'a Treasure) -> Self {
TreasureMap { treasure }
}
}
幸运的是,上述代码可以正常工作,完整代码如下:
fn main() {
let booty = Treasure { dubloons: 1000 };
let my_map = TreasureMap::new(&booty);
let your_map = my_map.clone();
println!("{:?}", booty.dubloons);
println!("{:?}", my_map.treasure);
println!("{:?}", my_map);
println!("{:?}", your_map);
}
#[derive(Debug)]
struct Treasure {
dubloons: u32,
}
#[derive(Clone, Debug)]
struct TreasureMap<'a> {
treasure: &'a Treasure,
}
impl<'a> TreasureMap<'a> {
fn new(treasure: &'a Treasure) -> Self {
TreasureMap { treasure }
}
}
输出结果:
$ cargo run
Compiling rust-test v0.1.0 (D:\CODE2\rust-test)
Finished dev [unoptimized + debuginfo] target(s) in 0.49s
Running `target\debug\rust-test.exe`
1000
Treasure { dubloons: 1000 }
TreasureMap { treasure: Treasure { dubloons: 1000 } }
TreasureMap { treasure: Treasure { dubloons: 1000 } }
如果你继续深入,就会感到一些痛苦,是时候让Rc
登场。回忆下在 教程14 介绍的Box
,Rc
和Box
类似,你可以对它们使用.clone()
,Rust
的引用检查器会在最后一个引用生命周期终止后清空内存,这看上去就像是个微型的垃圾回收器
Rc
的具体用法如下:
#![allow(dead_code)]
use std::rc::Rc;
fn main() {
let booty = Rc::new(Treasure { dubloons: 1000 });
let my_map = TreasureMap::new(booty);
let your_map = my_map.clone();
println!("{:?}", my_map);
println!("{:?}", your_map);
}
#[derive(Debug)]
struct Treasure {
dubloons: u32,
}
#[derive(Clone, Debug)]
struct TreasureMap {
treasure: Rc<Treasure>,
}
impl TreasureMap {
fn new(treasure: Rc<Treasure>) -> Self {
TreasureMap { treasure }
}
}
Rc
不支持跨线程使用,如果你尝试将TreasureMap
发送给另一个线程,Rust
会给出错误提示:
fn main() {
let booty = Rc::new(Treasure { dubloons: 1000 });
let my_map = TreasureMap::new(booty);
let your_map = my_map.clone();
let sender = std::thread::spawn(move || {
println!("Map in thread {:?}", your_map);
});
println!("{:?}", my_map);
sender.join();
}
输出:
[snipped]
error[E0277]: `Rc<Treasure>` cannot be sent between threads safely
--> crates/day-23/rc-arc/./src/rc.rs:9:18
|
9 | let sender = std::thread::spawn(move || {
| __________________^^^^^^^^^^^^^^^^^^_-
| | |
| | `Rc<Treasure>` cannot be sent between threads safely
10 | | println!("Map in thread {:?}", your_map);
11 | | });
| |_____- within this `[closure@crates/day-23/rc-arc/./src/rc.rs:9:37: 11:6]`
[snipped]
Arc
是Send
版的Rc
,含义是自动引用计数(Atomically Reference Counted
),你目前只需要知道Arc
可以弥补Rc
的不足且开销要稍大即可(对于从JavaScript
转来的你来说,这种开销可以忽略了)
在只读情况下Arc
是Rc
的一个替代方案,但如果你需要更改保存的值那就不一样了。改变跨线程的数据需要加锁,任何对Arc
数据的跨线程更改都会导致如下报错:
error[E0596]: cannot borrow data in an `Arc` as mutable
或
error[E0594]: cannot assign to data in an `Arc`
Mutex & RwLock
如果说Arc
是为了解决Send
,那么Mutex
和 RwLock
则可以说是为了解决Sync
Mutex
(Mutual Exclusion
)提供了对象锁,可以确保在同一时刻只能有一个读或写,RwLock
则是允许同一时刻任意次读和一次写。Mutex
的开销比RwLock
要小,但限制性更强
通过Arc<Mutex>
或Arc<RwLock>
,你可以安全的跨线程修改数据,在介绍Mutex
和RwLock
之前,先来介绍下parking_lot
parking_lot
parking_lot 提供了几个Rust
同步类型的替代方案,它承诺更快的性能和更少的代码,不过最重要的特性则是不需要管理Result
,Rust
的Mutex
和RwLock
会返回一个Result
,这个Result
是个不受欢迎的“噪音”
锁
如果你给被守卫的数据加上Mutex
或 RwLock
,你可以通过drop(guard)
或离开作用域来解除守卫。Rust
的块机制让限制守卫变得容易,下面的示例利用一个块来做守卫,使得在第8行自动的结束了守卫
fn main() {
let treasure = RwLock::new(Treasure { dubloons: 1000 });
{
let mut lock = treasure.write();
lock.dubloons = 0;
println!("Treasure emptied!");
}
println!("Treasure: {:?}", treasure);
}
异步
异步和futures
增加了另外一个守卫和锁的问题,实践中很容易写出守卫跨越异步边界的情况:
#[tokio::main]
async fn main() {
let treasure = RwLock::new(Treasure { dubloons: 100 });
tokio::task::spawn(empty_treasure_and_party(&treasure)).await;
}
async fn empty_treasure_and_party(treasure: &RwLock<Treasure>) {
let mut lock = treasure.write();
lock.dubloons = 0;
// Await an async function
pirate_party().await;
} // lock goes out of scope here
async fn pirate_party() {}
最好的解决方式是避免,在await
之前释放锁,如果你不能在代码层面避免,tokio
提供了它自己的 sync types ,万不得已的情况下可以使用它们,这倒不是考虑性能问题,而是从代码复杂性角度的考量
相关阅读
总结
RwLock
和 Mutex
给了你安全修改结构体字段的能力,Arc
和 Rc
按我的理解则是如何使用Rust
的核心,不过在实践中要避免过度使用