Skip to content

← Fearless Concurrency: Threads, Channels, Shared State step 11 of 24

Easy Primitives

Mutex and MutexGuard: unlocking with RAII

Repair the final ownership step of a shared-counter function:

pub fn concurrent_counter(increments: Vec<u64>, workers: usize) -> u64

The starter divides the increments into chunks, adds them through one Arc<Mutex<u64>>, and joins all worker threads. Return the sum by taking ownership of the final counter, without locking it again.

Treat workers == 0 as one. Empty input returns zero. Inputs are small enough for this thread-per-chunk design, and the total fits in u64. Some totals exceed u32::MAX, so keep the specified integer type.

Increments Workers Result
[2, 3, 4] 2 9
[2, 3, 4] 0 9
[] 5 0
[4000000000, 1000000000] 2 5000000000

The worker count is a limit for chunking, not a requirement to start idle threads. The starter uses chunks of size increments.len().div_ceil(workers.max(1)).max(1). This can create fewer threads than requested, and creates none for empty input.

A guard controls access to the protected value

Mutex<T> stores the data with the synchronization state. A successful lock operation returns a guard that permits access to the data:

use std::sync::Mutex;

let counter = Mutex::new(0u64);
{
    let mut guard = counter.lock().unwrap();
    *guard += 1;
} // Dropping the guard releases the lock.
assert_eq!(*counter.lock().unwrap(), 1);

This is RAII: cleanup belongs to a value's destructor. Normal scope exit, early return and panic unwinding drop the guard. Aborting the process does not unwind. Deliberately forgetting a guard can prevent its destructor from running and leave the lock held.

A guard is not released just because it has no further reads in the source. Its drop scope matters. Use a small block or drop(guard) when you need to release it before doing other work. The starter's temporary guard is dropped at the end of each addition statement, so it locks once per increment.

Why lock returns a Result

A panic during a critical section may leave protected data inconsistent. Poisoning reports that possibility. It is advisory and does not detect every panic. Calling expect on a poisoned lock panics; recovering safely requires deciding whether the protected invariants still hold.

The exercise assumes successful worker execution. If a worker panics, join().expect(...) reports failure instead of returning a partial total.

Unwrap the Arc before consuming the mutex

The starter ends with counter.into_inner(). Method lookup finds Mutex::into_inner through Arc's dereference, but that method consumes the mutex. Dereferencing a shared ownership handle cannot move its contents out, so the starter reports E0507.

Use the associated function Arc::into_inner(counter) first. It consumes that handle and returns Some(Mutex<u64>) when it was the last strong reference, or None otherwise. Outstanding weak references do not prevent this extraction.

In this starter, each worker owns one clone and drops it before finishing. After every worker is joined, only the original strong handle remains. Joining arbitrary worker code would not establish that fact if it stored extra clones elsewhere.

Once you own the mutex, call Mutex::into_inner on it and handle its poison result. This consumes the mutex without acquiring its lock. The two steps answer different questions: who owns the mutex, and whether its protected data has been marked poisoned.

Exclusive borrowing can also avoid locking

If a function already receives &mut Mutex<u64>, it can access the value through get_mut:

use std::sync::Mutex;

fn bump(counter: &mut Mutex<u64>, by: u64) {
    *counter.get_mut().unwrap() += by;
}

let mut counter = Mutex::new(4);
bump(&mut counter, 3);
assert_eq!(counter.into_inner().unwrap(), 7);

get_mut uses the exclusive borrow rather than acquiring the lock, while still reporting poison. Clippy's mut_mutex_lock flags unnecessary locking through this kind of parameter. This does not imply that guards and mutable references have identical behavior or that every lock operation incurs a syscall.

Avoid locking the same mutex again on the same thread while holding its guard. The standard mutex is not reentrant; that second call will not return normally and may panic or deadlock. Keep lock lifetimes visible and avoid calls that unexpectedly acquire a lock already held by the caller.

Submissions must compile, pass the cases and pass the Rust Clippy gate.