We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 6 of 24
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…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.