Skip to content

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

Medium Primitives

Send: what it actually guarantees

Count word frequencies in parallel and return (word, count) pairs sorted by word.

pub fn word_counts(words: Vec<String>, workers: usize) -> Vec<(String, usize)>

Split the input into workers chunks, count each chunk on its own thread, merge the partial counts, sort, return. Empty input returns []; treat workers == 0 as 1.

The tests run the same input at workers = 1, 3, 8 and expect identical output, so getting this right is not optional.

The starter fails, and the error message is the lesson

It wraps the data in Rc — the reference-counted pointer you already know — and hands a clone to each thread. That is exactly the right instinct and exactly the wrong type:

error[E0277]: `Rc<Vec<String>>` cannot be sent between threads safely
   |
   |     handles.push(thread::spawn(move || {
   |                  ------------- ^------
   |                  |             |
   |                  |             within this `{closure@...}`
   |                  required by a bound introduced by this call
   |
   = help: within `{closure@...}`, the trait `Send` is not implemented
           for `Rc<Vec<String>>`
   = note: required for `{closure@...}` to implement `Send`

Take a moment with the shape of that message, because trait-bound errors are the ones most worth learning to read. It says: the closure is not Send; the closure is not Send because one of the things it captures is not; and it names the exact type. Trait errors are usually a chain like this, and the useful line is the innermost the trait ... is not implemented for ....

What Send means

pub unsafe auto trait Send {}

“Ownership of this value may be transferred to another thread.” That is the whole definition.

It is an auto trait: the compiler derives it structurally, for every type in your program, without you asking. A struct is Send when all its fields are. You essentially never write impl Send.

It is unsafe to implement by hand because getting it wrong is not a type error, it is a data race — undefined behaviour, on a good day a wrong number and on a bad day a crash weeks later.

The interesting types are the ones that are not Send:

type why not
Rc<T> the refcount is a plain usize. Two threads cloning at once race on it. Too low and the value is freed while alive; too high and it leaks.
*const T, *mut T a raw pointer carries no safety story at all, so the compiler assumes the worst.
NonNull<T> same, and it is what most collections are built from internally.
MutexGuard<'_, T> POSIX requires that a mutex is unlocked by the thread that locked it, so the guard’s destructor is pinned to its thread.

Rc versus Arc is the cleanest illustration in the language of a performance decision expressed in the type system. They are the same data structure; the only difference is whether the count is updated with an atomic instruction. Rc is faster and single-threaded. Arc costs an atomic and crosses threads. Neither is “better”, and the compiler simply will not let you use the fast one incorrectly.

Two fixes, and one is better

Arc<Vec<String>> works: Arc is Send (given T: Send + Sync), the clones are cheap, and the code you already wrote otherwise compiles.

But look at what this function actually needs: read-only access to a slice, for the duration of the call. That is a borrow, and thread::scope from the previous problem gives you borrows across threads for free. No Arc, no atomic refcount, no allocation. Reach for Arc when you need shared ownership — when the data must outlive the function that created it — not as ceremony to satisfy 'static.

Either fix is accepted here. The scoped version is the one to internalise.

Why the output is sorted

This is a reusable trick, not a hack, so it is worth naming: the merge order of the partial maps is scheduler-dependent, and HashMap iteration order is unspecified anyway, so the only stable thing to return is a canonical ordering. Sorting at the boundary turns a nondeterministic computation into a deterministic result. Designing the observable output to be order-independent is how you make concurrent code testable at all — you will see the other half of that idea (index tagging, for when order is the answer) later in this track.

One correction worth recording

The standard library docs for Send have historically been read as implying raw pointers are Send. They are not — the Nomicon is explicit, and it is confirmed by the compiler: a struct containing a *mut u8 fails E0277 at a spawn boundary exactly like Rc does. Trust the error message.

Loading visualization…