Rust Concurrency â Threads, Channels & Mutex
Rust concurrency provides memory-safe threading through ownership and type system guarantees, preventing data races at compile time without a garbage collector or runtime overhead.
What You'll Learn
In this tutorial, you'll learn how Rust handles concurrency: creating threads with std::thread, message passing with channels, shared state with Mutex, the Send and Sync traits, and writing concurrent code that is guaranteed free of data races.
Why It Matters
Concurrency bugs â data races, deadlocks, race conditions â are among the hardest to debug. Rust's type system catches data races at compile time, making it the only mainstream language that guarantees thread safety. This is transformative for multi-threaded systems like web servers, databases, and real-time scanners.
Real-World Use
Tokio's async runtime uses thread pools for work stealing. The Servo browser engine uses Rust concurrency for parallel layout rendering. Database connection pools use Arc<Mutex<T>> for shared state. Durga Antivirus Pro scans files across dozens of threads with zero data races, guaranteed by the compiler.
flowchart LR
subgraph "Thread 1"
P1[Producer] --> C[Channel]
end
subgraph "Thread 2"
C -->|message| P2[Consumer]
end
subgraph "Shared State"
M[Mutex] -->|lock| D[Data]
end
T1[Thread A] -->|lock| M
T2[Thread B] -->|lock| M
Prerequisites: Rust Ownership, Closures, and Smart Pointers.
Spawning Threads
Rust's std::thread module provides OS threads.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("Child thread: iteration {}", i);
thread::sleep(Duration::from_millis(100));
}
});
for i in 1..=3 {
println!("Main thread: iteration {}", i);
thread::sleep(Duration::from_millis(150));
}
handle.join().expect("Child thread panicked");
println!("Both threads completed");
}
Expected output:
Main thread: iteration 1
Child thread: iteration 1
Child thread: iteration 2
Main thread: iteration 2
Child thread: iteration 3
Child thread: iteration 4
Main thread: iteration 3
Child thread: iteration 5
Both threads completed
Message Passing with Channels
Channels allow threads to communicate by sending messages, avoiding shared state.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let values = vec!["scanning", "analyzing", "detecting", "cleaning"];
for v in values {
tx1.send(format!("Worker 1: {}", v)).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
thread::spawn(move || {
let values = vec!["loading", "parsing", "matching", "reporting"];
for v in values {
tx.send(format!("Worker 2: {}", v)).unwrap();
thread::sleep(Duration::from_millis(150));
}
});
for received in rx {
println!("Got: {}", received);
}
}
Expected output:
Got: Worker 2: loading
Got: Worker 1: scanning
Got: Worker 2: parsing
Got: Worker 2: matching
Got: Worker 1: analyzing
Got: Worker 1: detecting
Got: Worker 2: reporting
Got: Worker 1: cleaning
Shared State with Mutex
Mutex<T> provides mutual exclusion for shared data across threads.
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 cnt = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = cnt.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap());
}
Expected output:
Final count: 10
Send and Sync Traits
Send allows ownership transfer between threads. Sync allows shared access through references.
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
// Rc is NOT Send -- cannot be transferred across threads
// Arc IS Send and Sync -- safe for multi-threaded use
// Mutex IS Send and Sync -- provides interior mutability across threads
fn verify_send_sync<T: Send + Sync>(_: &T) {
println!("Type is Send + Sync");
}
fn main() {
let arc_data = Arc::new(Mutex::new(String::from("shared data")));
verify_send_sync(&arc_data);
let (tx, rx) = mpsc::channel::<String>();
let data = Arc::clone(&arc_data);
thread::spawn(move || {
let mut val = data.lock().unwrap();
val.push_str(" modified by thread");
tx.send(val.clone()).unwrap();
});
let result = rx.recv().unwrap();
println!("Received: {}", result);
println!("Main: {}", *arc_data.lock().unwrap());
}
Expected output:
Type is Send + Sync
Received: shared data modified by thread
Main: shared data modified by thread
Scoped Threads
Scoped threads allow borrowing non-'static data safely.
use std::thread;
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
thread::scope(|s| {
s.spawn(|| {
let sum: i32 = numbers.iter().filter(|&&n| n % 2 == 0).sum();
println!("Even sum: {}", sum);
});
s.spawn(|| {
let sum: i32 = numbers.iter().filter(|&&n| n % 2 != 0).sum();
println!("Odd sum: {}", sum);
});
});
println!("All scoped threads completed, numbers: {:?}", numbers);
}
Expected output:
Even sum: 30
Odd sum: 25
All scoped threads completed, numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Concurrency and Security
Rust's concurrency model is inherently security-safe:
- No data races: The type system prevents unsynchronized concurrent mutation
- No deadlocks via type system: While Rust cannot prevent all deadlocks, patterns like
parking_lot::ReentrantMutexare explicit - Race condition reduction: Channels force message-based communication, reducing shared mutable state
- Panic safety: A panicking thread does not corrupt shared state (poisoning enables recovery)
Durga Antivirus Pro's multi-threaded scan engine uses these guarantees to process thousands of files per second with zero data races.
Common Mistakes
1. Moving Non-Send Types Across Threads
Rc<T> is not Send. Use Arc<T> for thread-safe reference counting.
2. Holding a Mutex Lock Across await Points
In async code, holding a mutex across .await can cause deadlocks. Use tokio::sync::Mutex instead.
3. Forgetting to Join Threads
Dropping a JoinHandle detaches the thread. Always join() to ensure completion and catch panics.
4. Creating Too Many Threads
Each thread has a stack (default 2MB on Linux). Use a thread pool ( rayon) for CPU-bound tasks, not raw threads.
5. Deadlocking with Multiple Mutexes
Lock two mutexes in different order in different threads. Always establish a consistent lock ordering.
Practice Questions
1. What is the difference between Send and Sync?
Send types can be transferred across threads. Sync types can be shared across threads through references. Most types are both, but Rc<T> is neither, and Mutex<T> is both.
2. What does a channel provide?
A channel provides message passing between threads. The sender (tx) sends values, the receiver (rx) receives them. Channels are multiple-producer, single-consumer by default.
3. How does Mutex ensure safety?
Mutex<T> provides interior mutability across threads. It blocks threads until the lock is acquired, ensuring only one thread accesses the data at a time. The lock is automatically released when the guard goes out of scope.
4. What is thread poisoning?
When a thread panics while holding a Mutex lock, the mutex becomes poisoned. Subsequent lock() calls return Err(PoisonError). This prevents accessing potentially inconsistent data.
5. Challenge: Build a concurrent file word counter that reads files in parallel using threads and channels, then aggregates results.
Mini Project: Concurrent Log Processor
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::time::Instant;
fn process_log_line(line: &str, error_count: &mut usize, warning_count: &mut usize) {
if line.contains("ERROR") {
*error_count += 1;
} else if line.contains("WARN") {
*warning_count += 1;
}
}
fn worker(id: usize, rx: mpsc::Receiver<String>, errors: Arc<Mutex<usize>>, warnings: Arc<Mutex<usize>>) {
for line in rx {
let mut e = errors.lock().unwrap();
let mut w = warnings.lock().unwrap();
process_log_line(&line, &mut e, &mut w);
}
println!("Worker {} finished", id);
}
fn main() {
let log_data = vec![
"INFO: System started",
"ERROR: Connection timeout",
"WARN: Disk space low",
"INFO: User logged in",
"ERROR: Database connection failed",
"WARN: Memory usage high",
];
let errors = Arc::new(Mutex::new(0usize));
let warnings = Arc::new(Mutex::new(0usize));
let (tx, rx) = mpsc::channel();
let rx2 = rx;
let e1 = Arc::clone(&errors);
let w1 = Arc::clone(&warnings);
let handle = thread::spawn(move || worker(1, rx2, e1, w1));
for line in log_data {
tx.send(line.to_string()).unwrap();
}
drop(tx);
handle.join().unwrap();
println!("Errors: {}, Warnings: {}", *errors.lock().unwrap(), *warnings.lock().unwrap());
}
FAQ
Related Concepts
What's Next
Explore Rust Async/Await for asynchronous I/O, and Unsafe Rust for raw thread primitives.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro