Rc and Arc implement 1vN ownership mechanism

Rc and Arc implement 1vN ownership mechanism

    • Observe reference count changes
    • one example
    • Multi-threaded powerless Rc
    • Arc

The Rust ownership mechanism requires that a value can only have one owner, which is fine in most cases, but consider the following situation:

  • In a graph data structure, multiple edges may own the same node, and the node should not be released until no edges point to it.
  • In multi-threading, multiple threads may hold the same data, but you are limited by Rust’s safety mechanism and cannot obtain a mutable reference to the data at the same time.

In order to solve this problem, Rust allows a data resource to have multiple owners at the same time through reference counting. This implementation mechanism is Rc and Arc, the former is suitable for single threads, and the latter is suitable for multi-threads. Both are mostly the same.

fn main() {<!-- -->
    let s = String::from("hello, world");
    // s is transferred to a here
    let a = Box::new(s);
    // Report an error! Here we continue to try to transfer s to b
    let b = Box::new(s);
}

It can be solved by using Rc

use std::rc::Rc;
fn main() {<!-- -->
    let a = Rc::new(String::from("hello, world"));
    let b = Rc::clone( & amp;a);

    assert_eq!(2, Rc::strong_count( & amp;a));
    assert_eq!(Rc::strong_count( & amp;a), Rc::strong_count( & amp;b))
}

Use Rc::new to create a new Rc< String > smart pointer and assign it to the variable a, which points to the underlying string data. When the smart pointer Rc< T > is created, it will also increase the reference count by 1. At this time, the associated function Rc::strong_count that obtains the reference count returns a value of 1 .

Then, we use Rc::clone to clone a copy of the smart pointer Rc< String > and increase the reference count of the smart pointer to 2. Since a and b are two copies of the same smart pointer, the result of obtaining the reference count through both of them is 2. The clone here is a shallow copy, which only copies the pointer and increases the reference count, without cloning the underlying data.

Observe changes in reference counts

use std::rc::Rc;
fn main() {<!-- -->
        let a = Rc::new(String::from("test ref counting"));
        println!("count after creating a = {}", Rc::strong_count( & amp;a));
        let b = Rc::clone( & amp;a);
        println!("count after creating b = {}", Rc::strong_count( & amp;a));
        {<!-- -->
            let c = Rc::clone( & amp;a);
            println!("count after creating c = {}", Rc::strong_count( & amp;c));
        }
        println!("count after c goes out of scope = {}", Rc::strong_count( & amp;a));


count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
}

There are a few points worth noting:

  • Since the variable c is declared inside the statement block, it will be released because it goes out of scope when leaving the statement block, so the reference count will be reduced by 1. In fact, this is due to the fact that Rc implements the Drop feature
  • The reference counts of the three smart pointers a, b, and c are the same, and share the underlying data, so any one can be used when printing the count
  • What cannot be seen is: when a and b go out of scope, the reference count will become 0, and eventually the smart pointer and the underlying string it points to will be cleaned up and released.

In fact, RC is an immutable reference to the underlying data, so you cannot modify the data through it, because of Rust’s borrowing rules: either there are multiple immutable borrows, or there is only one mutable borrow.

An example

use std::rc::Rc;

struct Owner {<!-- -->
   name: String,
   // ...other fields
}

struct Gadget {<!-- -->
   id: i32,
   owner: Rc<Owner>,
   // ...other fields
}

fn main() {<!-- -->
   // Create an `Owner` based on reference counting.
   let gadget_owner: Rc<Owner> = Rc::new(Owner {<!-- -->
       name: "Gadget Man".to_string(),
   });

   // Create two different tools that belong to the same owner
   let gadget1 = Gadget {<!-- -->
       id: 1,
       owner: Rc::clone( & amp;gadget_owner),
   };
   let gadget2 = Gadget {<!-- -->
       id: 2,
       owner: Rc::clone( & amp;gadget_owner),
   };

   // Release the first `Rc<Owner>`
   drop(gadget_owner);

   // Although we released the gadget_owner above, the owner information can still be used here
   // The reason is that before drop, there are three smart pointer references pointing to Gadget Man. The above is just
   // Drop one of the smart pointer references instead of dropping the owner data. There are two more outside
   // The reference points to the underlying owner data, and the reference count has not yet been cleared.
   // So the owner data can still be used
   println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
   println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);

   // At the end of the function, `gadget1` and `gadget2` are also released, and the final reference count returns to zero, and then the underlying
   //The data is also cleaned and released
}

Rc brief summary

  • Rc/Arc is an immutable reference. You cannot modify the value it points to. You can only read it. If you want to modify it, you need to cooperate with the internal mutability RefCell or mutex lock Mutex in the following chapters.
  • Once the last owner disappears, the resource will be automatically recycled. This life cycle is determined at compile time.
  • Rc can only be used within the same thread. If you want to use it for object sharing between threads, you need to use Arc
  • Rc is a smart pointer that implements the Deref feature, so you don’t need to unwrap the Rc pointer first and then use the T inside. Instead, you can use T directly, such as gadget1.owner.name in the above example.

Multi-threaded powerless Rc< T >

use std::rc::Rc;
use std::thread;

fn main() {<!-- -->
    let s = Rc::new(String::from("Multi-threaded Rover"));
    for _ in 0..10 {<!-- -->
        let s = Rc::clone( & amp;s);
        let handle = thread::spawn(move || {<!-- -->
           println!("{}", s)
        });
    }
}


error[E0277]: `Rc<String>` cannot be sent between threads safely

The superficial reason is that Rc cannot be safely transferred between threads. In fact, it is because it does not implement the Send feature, which is the key to transferring data between multiple threads. We will explain it in the multi-threading chapter.

Of course, there are deeper reasons: Because Rc needs to manage reference counting, but the counter does not use any concurrency primitives, it cannot implement atomic counting operations, which will eventually lead to counting errors.

Fortunately, the sky is the limit, and Rust provides us with Arc that has similar functions but is multi-thread safe.

Arc

Arc is the abbreviation of Atomic Rc, as the name suggests: atomic Rc smart pointer. Atomization is a concurrency primitive, which we will explain in depth in subsequent chapters. Here you only need to know that it can ensure that our data can be safely shared between threads.

Why not just use Arc directly, but also go the extra mile to get an Rc? Why don’t Rust’s basic data types and standard library data types automatically implement atomic operations? In this way, there is no thread unsafe problem.

The reason is that although atomization or other locks can bring thread safety, they will be accompanied by performance losses, and this performance loss is not small. Therefore, Rust gives you this choice. After all, the proportion of code that requires thread safety is actually not high. Most of the time the programs we develop are in one thread.
Arc and Rc have exactly the same API and are easy to modify:

use std::sync::Arc;
use std::thread;

fn main() {<!-- -->
    let s = Arc::new(String::from("Multi-threaded Rover"));
    for _ in 0..10 {<!-- -->
        let s = Arc::clone( & amp;s);
        let handle = thread::spawn(move || {<!-- -->
           println!("{}", s)
        });
    }
}

Arc and Rc are not defined in the same module. The former is introduced through use std::sync::Arc and the latter is introduced through >use std::rc::Rc

Both of these are read-only. If you want to make the internal data modifiable, you must use it with the internal variability RefCell or the mutex lock Mutex.