How Arc Works in Rust Praying for Rust

The Atomic reference count (Arc) type is a smart pointer that allows you to share immutable data between threads in a thread-safe manner. I hadn’t found a good explanation of how it worked, so I decided to try to write one. The first part introduces how and why to use Arc. If you already know this part and just want to know How it works, you can skip to the second part: “How does it Work.”

Why do you need to use Arc?

When you try to share data between threads, you need the Arc type to ensure that the shared type lives as long as the longest running thread. Consider the following example:

use std::thread;
use std::time::Duration;

fn main() {
  let foo = vec![0]; // creation of foo here
  thread::spawn(|| {
    thread::sleep(Duration::from_millis(20));
    println!("{:? }", &foo); 
  });
} // foo gets dropped here

// wait 20 milliseconds
// try to print foo
Copy the code

This code will not compile. We get an error saying that foo’s reference has outlived Foo itself. This is because Foo is dropped at the end of main, and the dropped value will be attempted in the generated thread 20 milliseconds later. That’s where Arc comes in. Atomic reference counting ensures that type foo is not discarded until all references to it have ended — so foo will still exist even after the main function ends. Now consider the following example:

use std::thread;
use std::sync::Arc;
use std::time::Duration;

fn main() {
  let foo = Arc::new(vec![0]); 
  let bar = Arc::clone(&foo);
  thread::spawn(move || {
    thread::sleep(Duration::from_millis(20));
    println!("{:? }", *bar);
  });
  
  println!("{:? }", foo);
} 
Copy the code

In this example, we can reference Foo in the (main) thread and also access its value after the (child) thread is generated.

How does it work?

Now that you know how to use Arc, let’s discuss how it works. When you call let foo = Arc::new(vec! [0]) when you create a VEC! [0] and an atomic reference count with a value of 1, and store them all in the same place on the heap (next to each other). The pointer to this data on the heap is stored in foo. Thus, foo is composed of a pointer to an object that contains vec! [0] and atomic count.

When you call let bar = Arc::clone(&foo), you are getting a reference to foo, dereferencing foo (a pointer to data stored on the heap), then finding the address to which foo points, and finding the values in it (vec! [0] and atomic count), add the atomic count by one, and finally point to veC! The pointer to [0] is stored in bar.

Arc::drop() is called when foo or bar is out of scope, and the atom count is reduced by one. If Arc::drop() finds that the atomic count is equal to zero, then the data on the heap to which it points (vec! [0] and atomic count) are cleaned and erased from the heap.

Atomic counting is a type that lets you modify and increment its value in a thread-safe way; All previous atomic type operations must be completed before any other operations can be performed on the atomic type. Thus it is called atomic (indivisible).

It is important to note that Arc can only contain immutable data. This is because Arc cannot guarantee against data contention if two threads attempt to modify the contained value at the same time. If you wish to modify the data, you should encapsulate a Mutex guard inside the Arc type.

Why do these things make Arc thread-safe?

Arc is thread-safe because it guarantees to the compiler that a reference to the data will live at least as long as the data itself. This is because every time you create a reference to data on the heap, the atomic count increases by one, and the data is discarded only if the atomic count equals zero (the atomic count decreases by one each time a reference leaves scope) – the difference between Arc and a plain Rc is the atomic count.

So what’s Rc for? Why not Arc for everything?

The reason is that atomic counting is an (expensive) variable type that the normal usize type doesn’t have. Not only does the atomic type take up more memory in the actual program, but the operations of each atomic type also take longer because it must allocate resources to maintain a queue for calls that read or write to itself, thereby ensuring atomicity. (Please ignore the logic of the last paragraph. Atomic counting should be implemented through the platform instruction set and there should be no maintenance queues. (Arc takes up more memory)