Table of Contents
Rust’s concurrency model leverages its ownership and borrowing rules to guarantee thread safety at compile time, eliminating data races without requiring a garbage collector. This approach ensures safe, high-performance parallelism with minimal runtime overhead.
Rust’s Concurrency Model
Rust uses the following mechanisms to manage concurrency:
- Ownership: Ensures exclusive mutable access to data.
- Borrowing: Governs how data is accessed via references.
- Lifetimes: Prevent dangling references across threads.
- Send/Sync Traits: Define which types are safe for threading.
How Ownership and Borrowing Prevent Data Races
A data race occurs when:
- Two threads access the same data concurrently.
- At least one access is a write.
- There’s no synchronization.
Rust’s rules make data races impossible in safe code:
1. Exclusive Mutability (&mut T
)
- Only one mutable reference (
&mut T
) can exist at a time, enforced by the borrow checker. - This prevents multiple threads from writing to the same data simultaneously.
Example:
let mut data = 0;
let r1 = &mut data; // OK: Mutable borrow
// let r2 = &mut data; // ERROR: Cannot borrow `data` as mutable more than once
2. No Shared Mutability Without Synchronization
- Shared references (
&T
) are read-only, safe for concurrent access. - To mutate shared data, synchronization primitives like
Mutex
are required:
Example:
use std::sync::Mutex;
let shared = Mutex::new(42);
let guard = shared.lock().unwrap(); // Exclusive access
*guard += 1; // Safe mutation
Thread-Safe Types: Send and Sync
- Send: A type can be safely transferred across threads (e.g.,
String
,Mutex<T>
). - Sync: A type can be safely shared between threads via references (e.g.,
&i32
,Arc<T>
).
Example: Spawning Threads:
use std::thread;
let value = String::from("hello"); // `String` is `Send`
thread::spawn(move || { // `move` transfers ownership
println!("{}", value); // Safe: no other thread can access `value`
}).join().unwrap();
Common Concurrency Tools
Tool | Purpose | Thread Safety Mechanism |
---|---|---|
Mutex<T> |
Mutual exclusion | Locks for exclusive access |
Arc<T> |
Atomic reference counting | Shared ownership across threads |
RwLock<T> |
Read-write lock | Multiple readers or one writer |
mpsc channels |
Message passing | Transfers ownership between threads |
Example: Shared State with Arc + Mutex:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1; // Mutex ensures exclusive access
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // Outputs 10
Why This Matters
- No runtime overhead: Safety checks occur at compile time.
- No garbage collector: Safe concurrency without GC pauses.
- Fearless parallelism: The compiler rejects unsafe patterns, enabling confident concurrent programming.
Key Takeaways
✅ Ownership rules prevent:
- Concurrent mutable access (no data races).
- Dangling references (via lifetimes).
✅ Send/Sync enforce thread safety at compile time.
🚀 Use Mutex
, Arc
, or channels for safe shared state.
Real-World Impact: Crates like rayon
(parallel iterators) and tokio
(async runtime) rely on these guarantees for robust concurrency.
Experiment: What happens if you try to share an Rc<T>
across threads?
Answer: Compile error! Rc<T>
is not Send
(not thread-safe). Use Arc<T>
instead.