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>
andArc<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:
let counter = Arc::new(Mutex::new(0));
: We create a newMutex<T>
that holds an integer. We wrap it in anArc<T>
to allow multiple threads to own it.let counter = Arc::clone(&counter);
: We clone theArc<T>
for each thread.let mut num = counter.lock().unwrap();
: Thelock
method acquires a lock on the mutex, blocking the current thread until it's able to do so. It returns aMutexGuard
, which is a smart pointer that dereferences to the data.*num += 1;
: We can now safely modify the data.- 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>
andArc<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.