Concurrency in Rust - Sharing State

While message passing is a great way to handle concurrency, there are times when you need multiple threads to access the same shared data. This is known as shared-state concurrency. However, it comes with risks, such as data races, where multiple threads try to read and write to the same memory location at the same time, leading to corrupted data.

Rust’s type system and ownership rules provide tools to manage shared state safely. The primary tool for this is the Mutex<T>.

Mutual Exclusion with Mutex<T>

A Mutex (short for “mutual exclusion”) is a concurrency primitive that ensures only one thread can access some data at any given time. To access the data, a thread must first signal that it wants access by acquiring the mutex’s “lock.” The lock is a data structure that is part of the mutex and keeps track of who has exclusive access. Once the thread is done, it “unlocks” the data, allowing another thread to acquire the lock.

In Rust, this is handled by the std::sync::Mutex<T> struct.

Let’s see how it works:

  1. You wrap your shared data in a Mutex<T>.
  2. Before using the data, you call the lock() method on the mutex.
  3. The lock() method blocks the current thread until it’s able to acquire the lock. It returns a Result. If another thread that held the lock panicked, lock() will return an error.
  4. If successful, lock() returns a smart pointer called MutexGuard. This smart pointer implements Deref to let you access the inner data and, crucially, Drop.
  5. When the MutexGuard goes out of scope, its drop implementation automatically releases the lock.

This RAII (Resource Acquisition Is Initialization) pattern makes it impossible to forget to release the lock, which is a common bug in other languages.

Sharing a Mutex<T> Between Threads

If you try to pass a Mutex<T> to multiple threads, you’ll run into a problem with the ownership rules. The first thread you pass it to will take ownership.

use std::sync::Mutex;
use std::thread;

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

    // This code will not compile!
    thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });

    thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });
}

To allow multiple threads to “own” the mutex, we need to combine it with another smart pointer: Arc<T>.

Arc<T>: Atomic Reference Counting

Arc<T> stands for Atomically Reference Counted. It’s a thread-safe version of Rc<T>. It lets multiple threads share ownership of a value, and it guarantees that the value will only be deallocated when the last reference to it is dropped. The reference counting is done using atomic operations, which makes it safe to use across threads.

The standard pattern for sharing mutable state in Rust is Arc<Mutex<T>>.

Let’s fix our counter example using this pattern.

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

fn main() {
    // Wrap the Mutex in an Arc to allow shared ownership across threads.
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        // Clone the Arc for each thread. This increases the reference count.
        let counter = Arc::clone(&counter);
        
        let handle = thread::spawn(move || {
            // Each thread locks the mutex to get access to the data.
            let mut num = counter.lock().unwrap();
            *num += 1;
            // The lock is automatically released when `num` goes out of scope.
        });
        handles.push(handle);
    }

    // Wait for all the threads to finish.
    for handle in handles {
        handle.join().unwrap();
    }

    // At the end, we can lock the mutex and print the final value.
    println!("Result: {}", *counter.lock().unwrap());
}

In this correct example:

  1. The Mutex is wrapped in an Arc.
  2. For each new thread, we clone the Arc. This creates a new pointer to the same Mutex and increments the atomic reference count.
  3. Each thread can now safely acquire the lock, mutate the data, and release the lock.
  4. The main thread waits for all threads to complete.
  5. The final result will be 10, as expected.

Conclusion

Sharing state between threads can be complex, but Rust provides powerful and safe tools to manage it. The Arc<Mutex<T>> pattern is the idiomatic way to allow multiple threads to read and write to shared data. By leveraging the type system and smart pointers, Rust helps you avoid common concurrency pitfalls like data races and forgetting to release locks, enabling “fearless concurrency.”