Skip to content

Q042. When should you use Arc, Mutex, RwLock, atomics, or message passing?

Short Answer

Choose by ownership and access pattern. Use Arc when several owners must keep one value alive. Add Mutex for short, exclusive updates to a compound invariant. Use RwLock only when reads dominate, read sections are meaningful, and measurements justify its extra complexity. Use atomics for small independent state with a precisely defined memory-ordering protocol. Prefer message passing when state should have one logical owner and operations can be expressed as commands.

Deep Dive

These primitives are not direct substitutes:

NeedTypical choice
Shared immutable ownershipArc<T>
Shared compound mutable stateArc<Mutex<T>>
Concurrent reads, infrequent writesArc<RwLock<T>>
Counter, flag, or pointer-sized stateArc<Atomic*>
One owner processes commandschannel plus owner task/thread

Start with the simplest model that makes the invariant obvious. A RwLock can be slower than a Mutex when critical sections are short or writes are frequent. Atomics avoid lock acquisition but move complexity into ordering and multi-variable consistency. Channels introduce queues, capacity decisions, and failure handling but can greatly simplify ownership.

Arc is often present around a lock because spawned workers need independent owners. It is unnecessary when scoped threads or ordinary borrows already prove that the data outlives all users.

Internal Model

A mutex serializes critical sections. An RwLock tracks reader and writer admission. Atomics coordinate individual memory locations through hardware-supported operations and memory ordering. A channel transfers messages through a synchronized queue, often waking a blocked thread or async task.

Performance depends on contention, cache-line movement, scheduler behavior, critical-section length, and workload distribution. The type name alone does not determine the fastest option.

Example

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

fn main() {
    let balances = Arc::new(Mutex::new((100_i64, 100_i64)));
    let worker_balances = Arc::clone(&balances);

    let transfer = thread::spawn(move || {
        let mut balances = worker_balances.lock().unwrap();
        balances.0 -= 10;
        balances.1 += 10;
    });

    transfer.join().unwrap();
    assert_eq!(*balances.lock().unwrap(), (90, 110));
}

Common Mistakes

  • Selecting RwLock merely because the application performs reads.
  • Replacing a multi-field invariant with unrelated atomics.
  • Sharing all state behind one global lock without considering ownership boundaries.

Follow-ups

  • Why can an RwLock underperform a Mutex?
  • What is backpressure in a bounded channel?
  • Which memory ordering is required for an atomic counter versus a publication flag?

References

  • Rust Standard Library
  • The Rust Performance Book