🎓 A1Learner
🔍

Rust Mutex & Arc

Arc<Mutex<T>> is the standard team for sharing one changing value between threads. Arc counts owners across threads, and a Mutex lets only the thread holding its lock touch the value inside.

Arc::clone gives each thread its own handle to the same Mutex

Mutex<T> is a value with a lock. Only one thread may hold it at a time. Reading or changing the value goes through the guard that .lock() returns. Arc<T> is an Rc whose count is updated atomically, safe for many threads:

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
    let counter = Arc::new(Mutex::new(0));
    let c = Arc::clone(&counter);
    let h = thread::spawn(move || { *c.lock().unwrap() += 10; });
    *counter.lock().unwrap() += 1;
    h.join().unwrap();
    println!("counter = {}", counter.lock().unwrap());
}

The guard unlocks itself when it is dropped

Usually at the end of the statement — there is no unlock call to forget:

use std::sync::Mutex;
fn main() {
    let m = Mutex::new(5);
    {
        let mut guard = m.lock().unwrap();
        *guard = 6;
    }
    println!("now = {}", *m.lock().unwrap());
}

A plain Mutex cannot be moved into thread after thread

This failure is why Arc enters the picture:

let m = Mutex::new(0);
for _ in 0..2 {
    thread::spawn(move || { *m.lock().unwrap() += 1; });
    // error E0382: m was moved into the first thread;
    // the second loop turn has nothing left to move
}

Rc is refused between threads — Arc is the fix

Rc<Mutex<T>> fails differently: Rc is not Send, so it may not travel between threads. Arc is the same shape, made safe to send:

let shared = Rc::new(Mutex::new(0));
thread::spawn(move || { *shared.lock().unwrap() += 1; });
// error E0277: Rc<Mutex<i32>> cannot be sent between threads safely

Poisoning: a panicking holder marks the lock

If a thread panics while holding the lock, later lock() calls return an Err. You can then decide whether the data is still trustworthy. If you judge it still usable, call into_inner() on the PoisonError: it hands you the guard despite the poisoning:

use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
    let m = Arc::new(Mutex::new(7));
    let c = Arc::clone(&m);
    let h = thread::spawn(move || {
        let _guard = c.lock().unwrap();
        panic!("holder panics while holding the lock");
    });
    let _ = h.join();
    let result = m.lock();
    match result {
        Ok(_) => println!("lock is fine"),
        Err(poisoned) => println!("poisoned: value inside = {}", *poisoned.into_inner()),
    }
}

Example

use std::sync::{Arc, Mutex}; use std::thread;
fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];
    for _ in 0..5 {
        let c = Arc::clone(&counter);
        handles.push(thread::spawn(move || { *c.lock().unwrap() += 1; }));
    }
    for h in handles { h.join().unwrap(); }
    println!("counter = {}", counter.lock().unwrap());
}

Key points

  • RwLock allows many readers or one writer.
  • Prefer it when reads greatly outnumber writes.
  • Keep locked sections short, and never hold a lock while awaiting (that lesson is ahead)
  • For one plain shared number, the std::sync::atomic types are simpler than a Mutex.
  • A Mutex earns its keep when the guarded data is larger.
  • Two locks taken in opposite orders can still freeze a program — the compiler cannot save you from that one.
  • Use one shared value for a counter, a cache, or a set of results when a single shared value is needed.
  • One changing value can be shared between threads when an Arc owns it and a Mutex guards it.

In this example, counter is an Arc around a Mutex around the number 0. The loop runs five times. Each time, Arc::clone gives one thread its own handle to the same counter. The thread locks the Mutex and adds 1 through the guard. Then the thread finishes. The guard drops at the end of the statement. That unlocks the Mutex for the next thread. main joins all five handles, then locks once more to print the final value: counter = 5. The lock is what makes five racing +1s add up correctly.

Note

Rust promises no data races, not no deadlocks.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What is lock poisoning?