Skip to main content

Shared-State Concurrency with Mutex<T> and Arc<T>

In our previous article, we explored Message Passing. Now, we'll look at another approach to concurrency: Shared-State Concurrency with Mutex<T> and Arc<T>.


📚 Prerequisites

  • A solid understanding of Rust's ownership system, threads, and message passing.
  • Familiarity with smart pointers.

🎯 Article Outline: What You'll Master

In this article, you will learn:

  • Mutexes: What a Mutex<T> is and how it allows you to safely share data between threads.
  • Atomic Reference Counting: How Arc<T> allows multiple owners of the same data.
  • Combining Mutex<T> and Arc<T>: How to use these two types together to share mutable data between threads.

🧠 Section 1: The Core Concepts of Shared-State Concurrency

Shared-state concurrency is a model of concurrency in which multiple threads can access the same shared data. This can be more complex than message passing, but it's also more flexible.

Key Principles:

  • Mutual Exclusion: A Mutex<T> (mutual exclusion) is a smart pointer that provides interior mutability. It ensures that only one thread can access the data at a time.
  • Atomic Reference Counting: An Arc<T> is a thread-safe reference-counting smart pointer. It allows multiple threads to own the same data.

💻 Section 2: Deep Dive - Implementation and Walkthrough

2.1 - Using a Mutex<T> to Allow Access to Data from One Thread at a Time

Here's an example of using a Mutex<T> to protect a counter that is shared across multiple threads:

// main.rs
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
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);
}

for handle in handles {
handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());
}

Step-by-Step Code Breakdown:

  1. let counter = Arc::new(Mutex::new(0));: We create a new Mutex<T> that holds an integer. We wrap it in an Arc<T> to allow multiple threads to own it.
  2. let counter = Arc::clone(&counter);: We clone the Arc<T> for each thread.
  3. let mut num = counter.lock().unwrap();: The lock method acquires a lock on the mutex, blocking the current thread until it's able to do so. It returns a MutexGuard, which is a smart pointer that dereferences to the data.
  4. *num += 1;: We can now safely modify the data.
  5. When num goes out of scope at the end of the closure, the lock is automatically released.

🔬 Section 4: A Deeper Dive: Mutex<T> and RefCell<T>

Mutex<T> is similar to RefCell<T> in that it provides interior mutability. However, Mutex<T> is designed to be used in a multi-threaded context, while RefCell<T> is for single-threaded contexts.


✨ Section 6: Best Practices and Anti-Patterns in Rust

Rust Best Practices (Idiomatic Way):

  • Do this: Use Mutex<T> to protect shared data.
  • And this: Use Arc<T> to share ownership of data across threads.

Anti-Patterns (What to Avoid in Rust):

  • Don't do this: Holding a lock for a long time. This can lead to performance problems and deadlocks.

💡 Conclusion & Key Takeaways

You've learned how to use Mutex<T> and Arc<T> to safely share mutable data between threads.

Let's summarize the key Rust takeaways:

  • Mutex<T> provides mutual exclusion.
  • Arc<T> provides shared ownership.
  • Combining Mutex<T> and Arc<T> is a powerful pattern for shared-state concurrency.

➡️ Next Steps

In the next article, "Sync and Send Traits: Extensible Concurrency", we'll look at the traits that make all of this possible.


Further Reading (Rust Resources)