Concurrency in Rust

Multi-threading in Rust

We can translate the C++ program in Rust.

We will decompose the solution to this problem in multiple steps.

Single-threading in Rust: First step

As in the C++ example, we need to lock the data manually with a mutex which stands for mutually exclusive. It is an advanced kind of reference which can only expose a writable (mutable) reference to a variable while it is locked.

Unlocking happens automatically after we leave the scope or expression, since mutex is a smart lock: it unlocks automatically when it is dropped (de-allocated) at the end of the spawn.

This means it can be used to share data in a program and control simultaneous access.

First, we try to solve the problem on one CPU, single-threaded.

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }
}

The expression m.lock() returns a mutable reference to the data. Without calling lock, it is simple impossible to refer to the interior of the mutex. At the end of the curly braces block, the lock is dropped automatically, since de-allocation means dropping the lock.

play

Multi-threading in Rust: First-step

When we try to make this application multi-threaded, we encounter a problem. We make multiple threads and call an expression in each thread to increase the counter. We still have a mutex, or a lock-able variable. However, it is moved inside one of the threads.

let counter = Mutex::new(0);
let mut handles = vec![];

for _ in 0..10 {
    let handle = thread::spawn(move || {
        // counter is moved into thread
        let mut num = counter.lock().unwrap();
        // counter cannot be used in next thread
        *num += 1;
    });
    handles.push(handle);
}

The error that we get says:

moved value counter cannot be used after move

play

Multi-threading in Rust: shared reference

To prevent one of the threads from capturing the mutex, we need to be able to share it in between multiple threads. The problem is that it’s ownership is moved and we loose access to it in the next thread. We need to work with multiple references to the mutex. To do this, we create a special kind of reference, a smart-pointer to the mutex.

The smart pointer (or reference) is called Arc which stands for “atomic reference counted”. It can be cloned infinitely many times.

let counter = Arc::new(Mutex::new(0));

Smart non-owning reference such as Arc do not move or capture ownership. We can now share copies or clones of this reference among the different threads. The copies of these references can be moved inside the threads and there is no problem anymore for the compiler.

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap();

        *num += 1;
    });
    handles.push(handle);
}

This time the access to the reference is synchronized using the Mutex and Arc in a software pattern called interior mutability.

play

This demonstrates the power of advanced types such as Arc and Mutex to make more reliable software.