How to Use std

:sync for Synchronization Primitives

Use Arc and Mutex from std::sync to safely share mutable data across threads in Rust.

Shared state across threads

You are building a concurrent job processor. Ten threads are churning through tasks, and they all need to update a shared progress counter. You try to share a plain integer, and the compiler screams. You try to share a reference, and the borrow checker blocks you because threads can outlive the scope that created them. You need a mechanism that says "this data belongs to everyone, but only one person touches it at a time."

Rust solves this with a combination of shared ownership and mutual exclusion. You wrap your data in a Mutex to serialize access, then wrap the mutex in an Arc to share the pointer across threads. This pattern appears everywhere in systems code: worker pools, event loops, caches, and configuration managers.

Arc and Mutex: The key and the lock

Arc handles the ownership. It stands for Atomic Reference Counted. Think of it like a set of master keys to a shared safe. You can duplicate the key as many times as you need. Every key points to the same safe. When the last key is destroyed, the safe is melted down and the memory is reclaimed. The "atomic" part means the key count updates are safe even if two threads drop keys at the exact same nanosecond. Arc uses atomic instructions to increment and decrement the counter, so the count never corrupts.

Mutex handles the access. It stands for Mutual Exclusion. It is the lock on the safe. You can hold a key, but you still need to turn the lock to get inside. Only one thread can hold the lock at a time. If thread A has the lock, thread B waits until A finishes and releases it. The mutex guarantees that no two threads can read or write the inner data simultaneously.

Convention aside: write Arc::clone(&counter) instead of counter.clone(). Both compile, but counter.clone() looks like you are deep-copying the data. The explicit form signals to readers that you are cloning the pointer, not the payload. This distinction matters when the payload is large.

Minimal example

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

/// Wraps the counter in a Mutex for exclusive access, then an Arc for shared ownership.
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

// Spawn ten threads, each getting a clone of the Arc key.
for _ in 0..10 {
    // Clone the Arc, not the integer. This bumps the atomic reference count.
    let counter = Arc::clone(&counter);
    
    // Move the Arc into the thread closure.
    let handle = thread::spawn(move || {
        // Acquire the lock before touching the data.
        // lock() returns a Result; unwrap panics if the mutex is poisoned.
        let mut num = counter.lock().unwrap();
        
        // Dereference the guard to mutate the integer.
        *num += 1;
        
        // The MutexGuard drops here, releasing the lock automatically.
    });
    
    handles.push(handle);
}

// Wait for all threads to finish.
for handle in handles {
    handle.join().unwrap();
}

// Read the final value.
println!("Result: {}", *counter.lock().unwrap());

Walkthrough: What happens under the hood

When you call Arc::new(Mutex::new(0)), Rust allocates a block on the heap. It stores the integer 0, the mutex state, and an atomic reference count starting at one. The Arc you hold in main is just a pointer to that heap block.

Inside the loop, Arc::clone increments the atomic counter and returns a new pointer. The cost is tiny: a single atomic instruction. You are not copying the integer. You are handing out another key.

thread::spawn takes ownership of the closure. The move keyword forces the closure to capture the Arc by value. This is crucial. If you did not move, the thread would try to borrow data from main, which might vanish before the thread starts. The compiler rejects borrowed references in spawn with E0373 (closure may outlive the current function) or similar lifetime errors. Moving the Arc guarantees the data stays alive as long as the thread runs.

Inside the thread, counter.lock() attempts to acquire the mutex. If another thread holds it, this call blocks until the lock is free. The return type is a LockResult<MutexGuard>. The MutexGuard is a RAII wrapper. It holds the lock. When num drops at the end of the closure, the guard drops, and the lock releases automatically. You never have to remember to unlock.

When all threads finish, join waits for them to complete. The final lock() call acquires the mutex one last time to read the value. The result is 10.

Keep the lock scope tight. The guard holds the lock for as long as it lives. If you leak the guard, the mutex stays locked forever.

Realistic example: A thread-safe cache

In real applications, you rarely share a single integer. You share complex structures like maps or lists. Here is a cache that multiple threads can read and write.

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;

/// A thread-safe cache shared across workers.
struct SharedCache {
    data: Mutex<HashMap<String, Vec<u8>>>,
}

impl SharedCache {
    fn new() -> Self {
        Self {
            data: Mutex::new(HashMap::new()),
        }
    }

    /// Insert a value into the cache.
    fn insert(&self, key: String, value: Vec<u8>) {
        // Lock the map, insert, and release.
        // The guard drops at the end of the block.
        self.data.lock().unwrap().insert(key, value);
    }

    /// Get a value from the cache.
    fn get(&self, key: &str) -> Option<Vec<u8>> {
        // Lock, get, and clone the value.
        // We must clone because the guard goes out of scope immediately.
        self.data.lock().unwrap().get(key).cloned()
    }
}

fn main() {
    let cache = Arc::new(SharedCache::new());
    let mut handles = vec![];

    // Writer threads.
    for i in 0..5 {
        let cache = Arc::clone(&cache);
        handles.push(thread::spawn(move || {
            let key = format!("item_{}", i);
            cache.insert(key, vec![i as u8; 10]);
        }));
    }

    // Reader threads.
    for i in 0..5 {
        let cache = Arc::clone(&cache);
        handles.push(thread::spawn(move || {
            let key = format!("item_{}", i);
            if let Some(data) = cache.get(&key) {
                println!("Thread {} got {} bytes", i, data.len());
            }
        }));
    }

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

The get method calls .cloned() on the iterator. This is necessary because get returns a reference tied to the MutexGuard. If you returned the reference directly, the compiler would reject it with E0515 (cannot return value referencing local data). The guard would drop before the reference could be used. Cloning the value breaks the borrow.

Send and Sync: The compiler's thread safety checks

Rust uses two marker traits to enforce thread safety: Send and Sync.

Send means a value can be transferred to another thread. Most types are Send. Rc is not Send because its counter is not atomic. If you try to move an Rc into a thread, the compiler rejects you with E0277 (trait bound not satisfied). Arc is Send because it uses atomic operations.

Sync means a reference to a value can be shared across threads. Mutex<T> is Sync even if T is not Sync, because the mutex serializes access. This allows you to share a Mutex<RefCell<T>> across threads, even though RefCell is not thread-safe on its own. The mutex makes the whole thing safe.

Convention aside: Mutex protects the data, but it does not prevent deadlocks. If you hold lock A and try to get lock B, and another thread holds B and tries A, you deadlock. Rust cannot detect this statically. You must enforce a global lock ordering or use a single coarse-grained lock.

Pitfalls: Deadlocks, poisoning, and scope

Deadlocks happen when threads wait for each other in a cycle. Rust does not prevent deadlocks. You have to design your locking strategy carefully. Always acquire locks in a consistent order. If you need multiple locks, define a hierarchy and never acquire a lower-level lock while holding a higher-level one.

Lock poisoning is another trap. If a thread panics while holding a lock, the mutex is marked poisoned. Subsequent lock() calls return Err. Calling unwrap() on a poisoned mutex panics the current thread. This turns a single bug into a total shutdown. Decide early: do you want to propagate the panic, or recover? lock().unwrap() propagates. lock().unwrap_or_else(|e| e.into_inner()) recovers the guard if you trust the data is still valid.

Scope leaks cause silent hangs. If you write let guard = mutex.lock().unwrap(); and then return early from a function, the guard drops and the lock releases. But if you accidentally extend the guard's lifetime, the lock stays held longer than expected. Other threads block waiting for a lock that will never be released.

Treat the lock scope as a critical section. Do I/O inside a lock, and you will regret it.

Decision: Choosing the right primitive

Use Arc<Mutex<T>> when you need shared mutable state across threads. This is the standard pattern for a shared counter, a cache, or a configuration object that updates occasionally.

Use Rc<RefCell<T>> when you have shared mutable state in a single-threaded context. Rc is faster than Arc because it skips atomic overhead. RefCell enforces borrowing rules at runtime instead of compile time.

Use AtomicUsize or AtomicBool when you need simple shared flags or counters without locking. Atomics are lock-free and faster for primitive updates, but they cannot protect complex data structures.

Use mpsc::channel when threads need to send data rather than share a mutable bucket. Channels push ownership from sender to receiver. This often eliminates the need for locks entirely.

Use RwLock when you have many readers and few writers. Mutex blocks readers when a writer holds the lock. RwLock allows multiple readers simultaneously, but only one writer.

Reach for channels first. Shared mutable state is the root of complexity. If you can send data instead of sharing it, your code will be easier to reason about.

Where to go next