Arc

You remember that we used an Rc to give a variable more than one owner. If we are doing the same thing in a thread, we need an Arc. Arc means "atomic reference counter". Atomic means that it uses the computer's processor so that data only gets written once each time. This is important because if two threads write data at the same time, you will get the wrong result. For example, imagine if you could do this in Rust:


#![allow(unused)]
fn main() {
// 🚧
let mut x = 10;

for i in 0..10 { // Thread 1
    x += 1
}
for i in 0..10 { // Thread 2
    x += 1
}
}

If Thread 1 and Thread 2 just start together, maybe this will happen:

  • Thread 1 sees 10, writes 11. Then Thread 2 sees 11, writes 12. No problem so far.
  • Thread 1 sees 12. At the same time, Thread 2 sees 12. Thread 1 writes 13. And Thread 2 writes 13. Now we have 13, but it should be 14. That's a big problem.

An Arc uses the processor to make sure this doesn't happen, so it is the method you must use when you have threads. You don't want an Arc for just one thread though, because Rc is a bit faster.

You can't change data with just an Arc though. So you wrap the data in a Mutex, and then you wrap the Mutex in an Arc.

So let's use a Mutex inside an Arc to change the value of a number. First let's set up one thread:

fn main() {

    let handle = std::thread::spawn(|| {
        println!("The thread is working!") // Just testing the thread
    });

    handle.join().unwrap(); // Make the thread wait here until it is done
    println!("Exiting the program");
}

So far this just prints:

The thread is working!
Exiting the program

Good. Now let's put it in a for loop for 0..5:

fn main() {

    let handle = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("The thread is working!")
        }
    });

    handle.join().unwrap();
    println!("Exiting the program");
}

This works too. We get the following:

The thread is working!
The thread is working!
The thread is working!
The thread is working!
The thread is working!
Exiting the program

Now let's make one more thread. Each thread will do the same thing. You can see that the threads are working at the same time. Sometimes it will say Thread 1 is working! first, but other times Thread 2 is working! is first. This is called concurrency, which means "running together".

fn main() {

    let thread1 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 1 is working!")
        }
    });

    let thread2 = std::thread::spawn(|| {
        for _ in 0..5 {
            println!("Thread 2 is working!")
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Exiting the program");
}

This will print:

Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 1 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Thread 2 is working!
Exiting the program

Now we want to change the value of my_number. Right now it is an i32. We will change it to an Arc<Mutex<i32>>: an i32 that can be changed, protected by an Arc.


#![allow(unused)]
fn main() {
// 🚧
let my_number = Arc::new(Mutex::new(0));
}

Now that we have this, we can clone it. Each clone can go into a different thread. We have two threads, so we will make two clones:


#![allow(unused)]
fn main() {
// 🚧
let my_number = Arc::new(Mutex::new(0));

let my_number1 = Arc::clone(&my_number); // This clone goes into Thread 1
let my_number2 = Arc::clone(&my_number); // This clone goes into Thread 2
}

Now that we have safe clones attached to my_number, we can move them into other threads with no problem.

use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));

    let my_number1 = Arc::clone(&my_number);
    let my_number2 = Arc::clone(&my_number);

    let thread1 = std::thread::spawn(move || { // Only the clone goes into Thread 1
        for _ in 0..10 {
            *my_number1.lock().unwrap() +=1; // Lock the Mutex, change the value
        }
    });

    let thread2 = std::thread::spawn(move || { // Only the clone goes into Thread 2
        for _ in 0..10 {
            *my_number2.lock().unwrap() += 1;
        }
    });

    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("Value is: {:?}", my_number);
    println!("Exiting the program");
}

The program prints:

Value is: Mutex { data: 20 }
Exiting the program

So it was a success.

Then we can join the two threads together in a single for loop, and make the code smaller.

We need to save the handles so we can call .join() on each one outside of the loop. If we do this inside the loop, it will wait for the first thread to finish before starting the new one.

use std::sync::{Arc, Mutex};

fn main() {
    let my_number = Arc::new(Mutex::new(0));
    let mut handle_vec = vec![]; // JoinHandles will go in here

    for _ in 0..2 { // do this twice
        let my_number_clone = Arc::clone(&my_number); // Make the clone before starting the thread
        let handle = std::thread::spawn(move || { // Put the clone in
            for _ in 0..10 {
                *my_number_clone.lock().unwrap() += 1;
            }
        });
        handle_vec.push(handle); // save the handle so we can call join on it outside of the loop
                                 // If we don't push it in the vec, it will just die here
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // call join on all handles
    println!("{:?}", my_number);
}

Finally this prints Mutex { data: 20 }.

This looks complicated but Arc<Mutex<SomeType>>> is used very often in Rust, so it becomes natural. Also, you can always write your code to make it cleaner. Here is the same code with one more use statement and two functions. The functions don't do anything new, but they move some code out of main(). You can try rewriting code like this if it is hard to read.

use std::sync::{Arc, Mutex};
use std::thread::spawn; // Now we just write spawn

fn make_arc(number: i32) -> Arc<Mutex<i32>> { // Just a function to make a Mutex in an Arc
    Arc::new(Mutex::new(number))
}

fn new_clone(input: &Arc<Mutex<i32>>) -> Arc<Mutex<i32>> { // Just a function so we can write new_clone
    Arc::clone(&input)
}

// Now main() is easier to read
fn main() {
    let mut handle_vec = vec![]; // each handle will go in here
    let my_number = make_arc(0);

    for _ in 0..2 {
        let my_number_clone = new_clone(&my_number);
        let handle = spawn(move || {
            for _ in 0..10 {
                let mut value_inside = my_number_clone.lock().unwrap();
                *value_inside += 1;
            }
        });
        handle_vec.push(handle);    // the handle is done, so put it in the vector
    }

    handle_vec.into_iter().for_each(|handle| handle.join().unwrap()); // Make each one wait

    println!("{:?}", my_number);
}