Introduction
Concurrency lets a program do multiple things at once, but it is notoriously hard to get right because of data races, deadlocks, and shared mutable state. Rust tackles this problem with what its creators call 'fearless concurrency': the same ownership and borrowing rules that prevent memory bugs also prevent data races, and the compiler catches most concurrency mistakes before the program ever runs. Rust offers OS threads via std::thread, message passing via channels, and shared-state concurrency via Mutex and Arc.
Cricket analogy: Running two net sessions at once is risky if two bowlers grab the same ball (data race) or two players wait on each other's turn forever (deadlock); Rust's fearless concurrency is like a ground with strict queueing rules enforced by an umpire, dedicated lanes (threads), a runner relaying scores (channels), and a single shared kit bag with one key (Mutex+Arc).
Syntax
use std::thread;
use std::sync::{Arc, Mutex};
use std::sync::mpsc;
// Spawn a thread
let handle = thread::spawn(move || {
// work done on a new OS thread
});
handle.join().unwrap();
// Channel for message passing
let (tx, rx) = mpsc::channel();
tx.send(42).unwrap();
let received = rx.recv().unwrap();
// Shared mutable state
let counter = Arc::new(Mutex::new(0));Explanation
thread::spawn takes a closure and runs it on a new OS thread, returning a JoinHandle; calling .join() blocks the current thread until the spawned thread finishes. Because the spawned thread may outlive the data it borrows, closures passed to spawn are usually written with the move keyword so they take ownership of any captured variables instead of borrowing them. For communication between threads, std::sync::mpsc::channel() creates a multi-producer, single-consumer channel: tx.send(value) pushes a value and rx.recv() blocks until one arrives. When threads need to share and mutate the same data, Mutex<T> wraps the data and only allows one thread at a time to access it through the guard returned by .lock(); to share ownership of that Mutex across threads you wrap it in Arc<T>, an atomically reference-counted pointer. Rc<T> is not safe to share across threads because its reference count is not updated atomically, so Rust requires Arc<T> instead. Under the hood, the Send and Sync marker traits tell the compiler which types are safe to move or share between threads, and it refuses to compile code that violates them, catching data races at compile time.
Cricket analogy: thread::spawn is like sending a fielder off to warm up in a separate net (new OS thread) with .join() being the captain waiting for that fielder to report back before play resumes; the mpsc channel is like a runner relaying scores from the boundary to the pavilion (tx.send/rx.recv), and Arc<Mutex<T>> is like a shared team scorebook only one player may write in at a time, safely tracked among all players (unlike Rc, which can't be shared across separate grounds).
Example
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
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());
}Output
Result: 5Key Takeaways
- thread::spawn creates an OS thread and returns a JoinHandle; call .join() to wait for it to finish.
- Use move closures to transfer ownership of captured variables into a spawned thread.
- mpsc::channel() provides multi-producer, single-consumer message passing via send/recv.
- Mutex<T> guards shared mutable state; Arc<T> shares ownership of that Mutex across threads (Rc<T> is not thread-safe).
- The Send and Sync traits let the compiler catch data races at compile time — this is 'fearless concurrency'.
Practice what you learned
1. What does thread::spawn return?
2. Why is Arc<T> used instead of Rc<T> for sharing a Mutex across threads?
3. What kind of channel does std::sync::mpsc::channel() create?
4. What does calling .lock() on a Mutex<T> return?
5. Why do closures passed to thread::spawn typically use the move keyword?
Was this page helpful?
You May Also Like
Ownership in Rust
Rust's core memory-safety system where every value has exactly one owner and is dropped when that owner goes out of scope.
Borrowing and References in Rust
How Rust lets code access data without taking ownership, using immutable and mutable references checked by the borrow checker.
Closures in Rust
Understand Rust closures, their capture modes, and the Fn, FnMut, and FnOnce traits that govern how they use captured variables.
Error Handling in Rust (Result)
Learn how Rust models recoverable errors with the Result enum and the ? operator instead of exceptions.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics