Skip to content

SOP: Concurrency ​

Updated Mar 2026

Overview ​

Rust prevents data races at compile time through the ownership system. This SOP covers threads, channels, mutexes, Arc, and the Send/Sync marker traits.

Concurrency Decision Tree ​

Spawning Threads ​

rust
use std::thread;
use std::time::Duration;

fn main() {
    // Spawn a new OS thread
    let handle = thread::spawn(|| {
        for i in 1..=5 {
            println!("Spawned thread: {i}");
            thread::sleep(Duration::from_millis(100));
        }
        42  // Return value
    });

    // Main thread continues
    for i in 1..=3 {
        println!("Main thread: {i}");
        thread::sleep(Duration::from_millis(150));
    }

    // Wait for spawned thread to finish
    let result = handle.join().unwrap();
    println!("Thread returned: {result}");
}

Move Closures ​

rust
use std::thread;

fn main() {
    let data = vec![1, 2, 3];

    // move forces the closure to take ownership
    let handle = thread::spawn(move || {
        println!("Vector: {:?}", data);
        // data is now owned by this thread
    });

    // println!("{:?}", data);  // ERROR: data was moved
    handle.join().unwrap();
}

Channels (Message Passing) ​

rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    // Clone transmitter for multiple producers
    let tx2 = tx.clone();

    thread::spawn(move || {
        tx.send("from thread 1".to_string()).unwrap();
    });

    thread::spawn(move || {
        tx2.send("from thread 2".to_string()).unwrap();
    });

    // Receive all messages
    for received in rx {
        println!("Got: {received}");
    }
}

Mutex (Mutual Exclusion) ​

rust
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;
            // MutexGuard dropped here — lock released
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Final count: {}", *counter.lock().unwrap());
    // Output: Final count: 10
}

RwLock (Read-Write Lock) ​

rust
use std::sync::{Arc, RwLock};
use std::thread;

fn main() {
    let data = Arc::new(RwLock::new(vec![1, 2, 3]));
    let mut handles = vec![];

    // Multiple readers can acquire simultaneously
    for i in 0..3 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let read_guard = data.read().unwrap();
            println!("Reader {i}: {:?}", *read_guard);
        }));
    }

    // Writer gets exclusive access
    {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let mut write_guard = data.write().unwrap();
            write_guard.push(4);
            println!("Writer: added 4");
        }));
    }

    for h in handles {
        h.join().unwrap();
    }
}

Send and Sync ​

TypeSendSyncThread-safe alternative
i32, String, Vec<T>YesYesN/A
Rc<T>NoNoArc<T>
RefCell<T>YesNoMutex<T> or RwLock<T>
Cell<T>YesNoAtomicU32, Mutex<T>
Arc<T>Yes (if T: Send+Sync)Yes (if T: Send+Sync)N/A
Mutex<T>Yes (if T: Send)Yes (if T: Send)N/A
Raw pointersNoNoWrap in struct, impl Send/Sync

Thread Pool Pattern ​

rust
use std::sync::{mpsc, Arc, Mutex};
use std::thread;

type Job = Box<dyn FnOnce() + Send + 'static>;

struct ThreadPool {
    workers: Vec<thread::JoinHandle<()>>,
    sender: mpsc::Sender<Job>,
}

impl ThreadPool {
    fn new(size: usize) -> Self {
        let (sender, receiver) = mpsc::channel::<Job>();
        let receiver = Arc::new(Mutex::new(receiver));
        let mut workers = Vec::with_capacity(size);

        for _ in 0..size {
            let receiver = Arc::clone(&receiver);
            workers.push(thread::spawn(move || loop {
                let job = receiver.lock().unwrap().recv();
                match job {
                    Ok(job) => job(),
                    Err(_) => break,  // Channel closed
                }
            }));
        }

        ThreadPool { workers, sender }
    }

    fn execute<F: FnOnce() + Send + 'static>(&self, f: F) {
        self.sender.send(Box::new(f)).unwrap();
    }
}

Deadlock Prevention ​

rust
// BAD — potential deadlock (inconsistent lock order)
fn transfer_bad(a: &Mutex<Account>, b: &Mutex<Account>, amount: f64) {
    let mut a_lock = a.lock().unwrap();
    let mut b_lock = b.lock().unwrap();  // Deadlock if another thread locks b then a
    // ...
}

// GOOD — consistent lock order by address
fn transfer_good(a: &Mutex<Account>, b: &Mutex<Account>, amount: f64) {
    let (first, second) = if std::ptr::addr_of!(*a) < std::ptr::addr_of!(*b) {
        (a, b)
    } else {
        (b, a)
    };
    let mut first_lock = first.lock().unwrap();
    let mut second_lock = second.lock().unwrap();
    // Transfer safely
}

// GOOD — use try_lock to detect deadlocks
fn try_transfer(a: &Mutex<Account>, b: &Mutex<Account>) -> Result<(), &'static str> {
    let mut a_lock = a.lock().map_err(|_| "poisoned")?;
    match b.try_lock() {
        Ok(mut b_lock) => { /* transfer */ Ok(()) }
        Err(_) => Err("would deadlock"),
    }
}

Checklist ​

  • [ ] Determine if you need message passing (channels) or shared state (Mutex/RwLock)
  • [ ] Use Arc for multi-threaded shared ownership (never Rc)
  • [ ] Use move closures when spawning threads that capture data
  • [ ] Keep lock scopes minimal — drop MutexGuard as soon as possible
  • [ ] Lock in consistent order to prevent deadlocks
  • [ ] Prefer channels over shared state when possible (simpler reasoning)
  • [ ] Use RwLock instead of Mutex for read-heavy workloads
  • [ ] Verify all types crossing thread boundaries are Send

Built with VitePress | Rust SOP Documentation