Skip to content

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

Easy Primitives

Arc gives you sharing, not mutability

Every worker computes the sum of its chunk and pushes it into one shared Vec. Return that vector, sorted ascending.

pub fn collect_into_shared(values: Vec<i64>, workers: usize) -> Vec<i64>

Chunk with values.chunks(values.len().div_ceil(workers)). Empty input gives []; treat workers == 0 as 1.

“I wrapped it in an Arc, why can’t I mutate it?”

This is the most-asked question in Rust concurrency, and the starter asks it for you:

error[E0596]: cannot borrow data in an `Arc` as mutable
   |
   |             sink.push(sum);
   |             ^^^^ cannot borrow as mutable
   |
   = help: trait `DerefMut` is required to modify through a dereference,
           but it is not implemented for `Arc<Vec<i64>>`

The answer is in the second half of that message. Arc<T> implements Deref but deliberately not DerefMut. All it can ever give you is a &T.

And it must be that way. Arc exists so that several handles point at one value. If it handed out &mut T, two threads could hold &mut to the same data simultaneously, and aliasing-xor-mutation — the rule underneath the entire borrow checker — would be broken by a library type. Arc cannot offer mutation without giving up the thing it exists for.

Two orthogonal questions

The framing that fixes this permanently:

  • Arc answers “who owns this?” — several handles, freed when the last one drops. It is about lifetime.
  • Mutex answers “who may touch it right now?” — one thread at a time, enforced. It is about access.

They are independent concerns, so you compose them. And once you see it that way, the notorious type stops being a magic incantation:

Arc<Mutex<Vec<i64>>>

Read outside-in: shared ownership of a lock protecting a vector. Every layer is doing one job.

Why never Mutex<Arc<T>>

Learners write this by accident, and it type-checks, which is why it is worth naming. Mutex<Arc<Vec<i64>>> is a lock protecting a handle. You can lock it and swap which allocation the handle points to — but the Vec inside is still reached through an Arc, so it is still immutable, and the mutex is guarding the wrong thing. It also cannot be shared, because the Mutex is the outermost type and there is only one of it.

If you find yourself writing Mutex<Arc<...>>, you almost certainly wanted the letters the other way round.

Why the result is sorted, and why that is not the real answer

The order in which workers reach push is the scheduler’s business, not yours. Run the same input twice and you can get [26, 10, 19] then [10, 26, 19]. Sorting collapses every possible interleaving onto one value, which makes the function testable.

Do not conclude that sorting is the technique. It works here only because the answer happens to be a set of numbers. When order carries meaning — a parallel map where output i must correspond to input i — sorting destroys the information you needed. The general answer is index tagging: send (index, result) and reassemble by index. That gets a problem of its own later in this track, and it is the one to remember.

Recovering the data at the end

You have an Arc<Mutex<Vec<i64>>> and you need to return a Vec<i64>. Three reasonable ways, all fine here:

  • lock it and clone() the contents;
  • lock it and std::mem::take(&mut *guard), leaving an empty Vec behind and avoiding the copy;
  • Arc::into_inner(shared) (which returns Some only when yours is the last handle — true, because every worker has been joined) followed by Mutex::into_inner.

The last one is the most interesting: both into_inners take self by value, and owning the container is already proof that nobody else can be looking, so neither needs to lock or synchronise anything.

Loading visualization…