Concurrency in Rust
Concurrency in Rust enables executing multiple tasks simultaneously while preventing data races at compile time through its ownership system. Rust offers thread-based parallelism, async/await for cooperative multitasking, and message passing between threads.
1. Threads
Threads in Rust provide OS-level parallelism. Each thread has its own stack and runs independently, scheduled by the operating system. Rust's ownership system ensures thread safety.
use std::thread;
use std::time::Duration;
fn main() {
// Spawn a new thread
let handle = thread::spawn(|| {
for i in 1..5 {
println!("Thread: {}", i);
thread::sleep(Duration::from_millis(1));
}
});
// Main thread work
for i in 1..3 {
println!("Main: {}", i);
thread::sleep(Duration::from_millis(1));
}
// Wait for thread to finish
handle.join().unwrap();
}
Key Points: thread::spawn creates a new thread. join() waits for thread completion. The main thread exits when it finishes unless you join spawned threads. Threads are expensive to create and switch between.
Thread Quiz
What happens if you don't call join() on a thread handle?
2. Shared-State Concurrency
Shared memory between threads requires synchronization. Rust provides Mutex (mutual exclusion) and Arc (atomic reference counting) for thread-safe shared access.
use std::sync::{Arc, Mutex};
fn main() {
// Arc = Atomic Reference Counting
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!("Final count: {}", *counter.lock().unwrap());
}
Safety Mechanisms: Arc enables multiple ownership across threads. Mutex ensures only one thread accesses data at a time. The compiler prevents data races at compile time.
Shared-State Quiz
Why do we need both Arc and Mutex for shared state?
3. Message Passing with Channels
Channels enable thread communication by sending messages. Rust's standard library provides multiple producer, single consumer (mpsc) channels.
use std::sync::mpsc; // Multiple Producer, Single Consumer
fn main() {
let (tx, rx) = mpsc::channel();
// Spawn thread to send message
thread::spawn(move || {
let val = String::from("hello");
tx.send(val).unwrap();
});
// Receive message in main thread
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
Channel Characteristics: Sending moves ownership of values. Channels can buffer multiple messages. Receiving blocks until a message arrives. Multiple senders can share one receiver.
Channel Quiz
What happens if all senders are dropped and the channel is empty?
4. Async Programming
Async/await provides lightweight concurrency using cooperative multitasking. Tasks yield control during I/O waits, enabling high concurrency with less overhead than threads.
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
sleep(Duration::from_secs(1)).await;
println!("Async task completed");
});
println!("Main thread continues working");
handle.await.unwrap();
}
Async Benefits: Much lighter weight than threads. Excellent for I/O-bound tasks. Built on futures that represent pending computations. Requires an async runtime like Tokio.
Async Quiz
When does an async task yield control back to the runtime?
5. Synchronization Primitives
Synchronization tools coordinate thread execution. Rust provides RwLock for multiple readers, Barrier for thread rendezvous, and more.
use std::sync::{RwLock, Barrier};
fn main() {
// Read-Write Lock
let lock = RwLock::new(5);
{
let r1 = lock.read().unwrap();
let r2 = lock.read().unwrap(); // Multiple readers allowed
}
{
let mut w = lock.write().unwrap(); // Only one writer
*w += 1;
}
// Barrier synchronization
let barrier = Arc::new(Barrier::new(3));
let mut handles = vec![];
for _ in 0..3 {
let b = Arc::clone(&barrier);
handles.push(thread::spawn(move || {
println!("Before wait");
b.wait();
println!("After wait");
}));
}
for h in handles {
h.join().unwrap();
}
}
Primitive Uses: RwLock is great for read-heavy workloads. Barriers synchronize thread phases. Condvars enable waiting for conditions. Always prefer the least restrictive primitive needed.
Sync Quiz
What's the advantage of RwLock over Mutex?
6. Atomic Types
Atomic operations provide lock-free thread-safe access for primitive types. They're implemented using processor instructions for maximum performance.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn main() {
let atomic_counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&atomic_counter);
handles.push(thread::spawn(move || {
counter.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles {
h.join().unwrap();
}
println!("Atomic counter: {}", atomic_counter.load(Ordering::SeqCst));
}
Atomic Characteristics: No locking overhead. Limited to simple operations. Memory ordering affects visibility guarantees. Perfect for counters and flags.
Atomic Quiz
What does Ordering::SeqCst guarantee?
7. Advanced Async Patterns
Async patterns solve common concurrency problems. Channels, semaphores, and select macros enable complex async workflows.
use tokio::sync::{Semaphore, mpsc};
#[tokio::main]
async fn main() {
// Async channel
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("message").await.unwrap();
});
if let Some(msg) = rx.recv().await {
println!("Received: {}", msg);
}
// Rate limiting with semaphore
let sem = Arc::new(Semaphore::new(3)); // Allow 3 concurrent
for i in 0..10 {
let permit = sem.clone().acquire_owned().await.unwrap();
tokio::spawn(async move {
// Critical section
println!("Task {} working", i);
tokio::time::sleep(Duration::from_secs(1)).await;
drop(permit); // Release semaphore
});
}
}
Pattern Benefits: Async channels enable communication between tasks. Semaphores limit concurrent access. Select waits on multiple operations. Buffering prevents backpressure.
Async Patterns Quiz
When would you use a semaphore in async code?