给前端看的Rust教程(23)Rc、Arc、Mutex、RwLock

4,839 阅读5分钟

原文:24 days from node.js to Rust

前言

在之前22篇文章里你接触到了Rust的基础核心概念,你知道了在Rust中一个数值只能有一个所有者,你还知道了生命周期的概念。这些内容都稍微有点独特和奇怪,但到这里我相信你已经开始学会接受了

不过当你开始一个项目后,你可就能会陷入到引用和生命周期注释的困境中,Rust中的SyncSend、生命周期、数值借用等会让你想要放弃

而我们接下来介绍的RcArcMutexRwLock将会在一定程度上解救你

正文

Rc & Arc

Rc 和 ArcRust中的引用计数类型,你可以通过计数引用来管理内存

假定这样一个场景,你有一个需要在多个数据结构中指向的数值。例如你正在开发一款关于太空海盗的游戏,它拥有高科技的藏宝图,并且指向宝藏的实际位置,自始至终我们的地图都需要保持一个对宝藏的引用,对应的代码可能如下:

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 介绍的BoxRcBox类似,你可以对它们使用.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]

ArcSend版的Rc,含义是自动引用计数(Atomically Reference Counted),你目前只需要知道Arc可以弥补Rc的不足且开销要稍大即可(对于从JavaScript转来的你来说,这种开销可以忽略了)

在只读情况下ArcRc的一个替代方案,但如果你需要更改保存的值那就不一样了。改变跨线程的数据需要加锁,任何对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

MutexMutual Exclusion)提供了对象锁,可以确保在同一时刻只能有一个读或写,RwLock则是允许同一时刻任意次读和一次写。Mutex的开销比RwLock要小,但限制性更强

通过Arc<Mutex>Arc<RwLock>,你可以安全的跨线程修改数据,在介绍MutexRwLock之前,先来介绍下parking_lot

parking_lot

parking_lot 提供了几个Rust同步类型的替代方案,它承诺更快的性能和更少的代码,不过最重要的特性则是不需要管理ResultRustMutexRwLock会返回一个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的核心,不过在实践中要避免过度使用