24 days from Node.js to Rust

preface

In the previous 22 articles you have been exposed to the basic core concepts of Rust. You have learned that there can only be one owner of a value in Rust, and you have learned about the concept of the lifecycle. It’s all slightly unique and strange, but by now I’m sure you’re starting to get used to it

But when you start a project, you can get bogged down in references and lifecycle comments. Sync, Send, lifecycle, numerical borrowing, etc. in Rust make you want to give up

Rc, Arc, Mutex, RwLock will save you to some extent

The body of the

Rc & Arc

Rc and Arc are the reference counting types in Rust that allow you to manage memory by counting references

Consider a scenario where you have a number that you need to point to in multiple data structures. For example, if you’re developing a game about space pirates, it has a high-tech treasure map that points to the actual location of the treasure, and all the time our map needs to keep a reference to the treasure, the code might look like this:

let booty = Treasure { dubloons: 1000 };

let my_map = TreasureMap::new(&booty);
let your_map = my_map.clone();
Copy the code

The content of the Treasure structure is fairly straightforward:

#[derive(Debug)]
struct Treasure {
    dubloons: u32,}Copy the code

The TreasureMap structure stores a reference in which the Rust compiler prompts for lifecycle annotations. A beginner to Rust might accept the compiler’s prompts, but is it right to follow the compiler’s prompts?

#[derive(Clone, Debug)]
struct TreasureMap<'a> {
    treasure: &'a Treasure,
}

impl<'a> TreasureMap<'a> {
    fn new(treasure: &'a Treasure) -> Self {
        TreasureMap { treasure }
    }
}
Copy the code

Fortunately, the code above works, and the complete code is as follows:

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 }
    }
}
Copy the code

Output result:

$cargo run Compiling rust-test v0.1.0 (D:\CODE2\rust-test) Finished dev [unoptimized + debuginfo] target(s)in0.49s Running 'target' \debug\rust-test.exe '1000 Treasure {dubloons: 1000} TreasureMap {Treasure: Treasure { dubloons: 1000 } } TreasureMap { treasure: Treasure { dubloons: 1000 } }Copy the code

If you go any further, you’re going to feel some pain, and it’s time for Rc. Recall from tutorial 14 that Box Rc is similar to Box, with which you can use.clone(), Rust’s reference checker empties memory after the last reference life cycle has expired, making it look like a mini-garbage collector

Rc is used as follows:

#! [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 }
    }
}
Copy the code

Rc does not support cross-thread use, and Rust will give you an error if you try to send TreasureMap to another thread:

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();
}
Copy the code

Output:

[snipped]
error[E0277]: `Rc<Treasure>` cannot be sent between threads safely
   --> crates/day-23/rc-arc/./src/rc.rs:9:18
    |
9   |       letsender = 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]Copy the code

Arc is the Atomically Reference Counted Rc version of Send, so all you need to know is that Arc can make up for Rc at a slightly higher cost (which is negligible for you coming from JavaScript).

Arc is an alternative to Rc in read-only cases, but not if you need to change the saved values. Changing cross-thread data requires locking, and any cross-thread changes to Arc data will result in the following error:

Error [E0596]: Cannot borrow data in an 'Arc' as mutable or error[E0594]: Cannot assign to data in an 'ArcCopy the code

Mutex & RwLock

If Arc is for Send, Mutex and RwLock are for Sync

Mutex (Mutual Exclusion) provides object locking to ensure that only one read or write can be available at a time, while RwLock allows any number of reads and writes at a time. Mutex is less expensive than RwLock, but more restrictive

Using Arc

or Arc

, you can safely modify data across threads. Before introducing Mutex and RwLock, let’s look at parking_lot

parking_lot

Parking_ lot provides several alternatives to the Rust synchronization type. It promises faster performance and less code, but the most important feature is that instead of managing results, Rust’s Mutex and RwLock return a Result that is unwelcome “noise.”

The lock

If you add Mutex or RwLock to guarded data, you can remove the guard by dropping (guard) or leaving scope. Rust’s block mechanism makes it easy to restrict guarding, and the following example uses a block for guarding, which automatically terminates guarding at line 8

fn main() {
  let treasure = RwLock::new(Treasure { dubloons: 1000 });

  {
      let mut lock = treasure.write();
      lock.dubloons = 0;
      println!("Treasure emptied!");
  }

  println!("Treasure: {:? }", treasure);
}
Copy the code

asynchronous

Asynchrony and futures add another guard and lock problem. In practice it is easy to write a guard crossing asynchronous boundaries:

#[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() {}
Copy the code

The best solution is to avoid, release locks before await, if you can’t avoid at the code level, Tokio provides its own Sync types that can be used as a last resort, not for performance reasons but for code complexity reasons

reading

  • std::rc
  • std::rc::Rc
  • std::sync::Arc
  • std::sync::Mutex
  • std::sync::RwLock
  • parking_lot
  • Tokio Sync

conclusion

RwLock and Mutex give you the ability to safely modify structure fields. Arc and Rc use the core of Rust as I understand it, but avoid overusing it in practice