Skip to content

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

Easy Primitives

Arc: shared ownership across threads

Share one allocation between workers detached threads, sum a disjoint slice in each, and report the total and the reference count afterwards.

pub fn share_and_reduce(data: Vec<i64>, workers: usize) -> (i64, usize)

Wrap data in an Arc, hand a clone to each worker, spawn with thread::spawn (not scope — this problem is about ownership), join everything, and return (total, Arc::strong_count(&shared)).

Treat workers == 0 as 1. Use the balanced split from earlier: base = len / workers, extra = len % workers, first extra slices one longer. Empty input gives (0, 1).

Say the count out loud before you write it

The second element of the tuple is always 1, for every input, every worker count, every run. Work out why before you read on — it is the whole model in one number.

Because: you created one Arc, cloned it workers times, each clone was moved into a thread, each thread ended, and when a thread ends its captured values are dropped, which decrements the count. By the time every join returns, the only handle left is the one you are holding. strong_count reads 1.

This is not a coincidence you should lean on generally. Reading strong_count while other threads are alive is a race — the value is stale the instant you get it, since another thread can clone or drop between the read and your use of it. It is deterministic here only because every join has completed. That constraint is the reason it is a fair test case rather than a trick.

What Arc is

Arc<T> — atomically reference counted — is shared ownership. The value lives on the heap next to a counter; clone bumps the counter and hands back another handle; drop decrements; the last one out frees the value. Every handle derefs to &T.

It is Rc with the counter updated by atomic instructions instead of plain ones, which is precisely why Arc: Send + Sync (given T: Send + Sync) and Rc is neither. That is the whole difference. Arc is not “the thread-safe smart pointer” in any deeper sense — see the next problem for what it is emphatically not.

The starter’s error

error[E0382]: use of moved value: `shared`
   |
   |     for i in 0..workers {
   |     ------------------- inside of this loop
   |         handles.push(thread::spawn(move || shared[start..end]...));
   |                                    ^^^^^^^ ------ use occurs due to use in closure
   |                                    |
   |                                    value moved here, in previous iteration of loop

move moved the Arc handle itself into the first closure. On the second iteration there is nothing left. The fix is not to remove move — the thread genuinely needs to own something — it is to give each thread its own handle:

let mine = Arc::clone(&shared);
handles.push(thread::spawn(move || { ... mine ... }));

Cloning an Arc does not clone the data. It is one atomic increment and a pointer copy, whatever T is. The Vec<i64> is allocated exactly once no matter how many workers you spawn.

Arc::clone(&x) versus x.clone()

You will see the explicit form far more often, and the reason given is readability: Arc::clone(&shared) says “a new handle, cheap” at a glance, while shared.clone() looks like it might be duplicating a Vec<i64>.

Be aware this is a convention, not a rule. clippy::clone_on_ref_ptr would enforce it, but it is in the restriction group — off by default, and it will not fire on this problem either way. A vocal minority now argues .clone() is fine and that the type is what tells you the cost. Pick one and be consistent; do not tell a colleague the compiler requires it.

One lint that will bite you if you get clever

A tempting way to make workers handles at once:

let handles = vec![Arc::new(data); workers];
error: initializing a reference-counted pointer in `vec![elem; len]`
  = note: each element will point to the same `Arc` instance

clippy::rc_clone_in_vec_init is warn-by-default and therefore a failure under this gate. vec![elem; n] evaluates elem once and clones it, so this is n handles to one allocation — occasionally what you want, and almost never what someone writing that line meant. Clone in a loop, or hoist the Arc::new to its own binding first so the intent is visible.

Loading visualization…