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 strings using worker threads, combine their counts, and return one
(word, count) pair per distinct string, sorted by word.
pub fn word_counts(words: Vec<String>, workers: usize) -> Vec<(String, usize)>
Each input string is one word for this exercise. Do not split it, trim it or change its case. Empty strings and strings containing spaces are valid keys. Sort using Rust's ordinary string ordering, not a locale-specific collation.
For ["b", "a", "b"], return [("a", 1), ("b", 2)], regardless of the
worker count. Empty input returns an empty vector. Treat zero workers as one.
For nonempty input, calculate the chunk size as
words.len().div_ceil(workers.max(1)). Process each nonempty chunk in a
worker and merge its counts. There can be fewer chunks than requested
workers; do not create empty workers just to reach that number.
Follow the error to the captured value
The starter puts the vector in Rc and moves a cloned handle into each
thread::spawn closure. The compiler reports E0277 because
Rc<Vec<String>> does not implement Send.
Read the diagnostic as a chain: spawning requires a Send closure; the
closure captures an Rc; that captured value is not Send. The repair
concerns how the input is shared, not the spelling of the closure.
What Send guarantees
Send means a value can be transferred to another thread safely. It does
not promise that the value can be accessed concurrently through shared
references; Sync describes that separate capability.
Send is an auto trait. The compiler normally infers it from a type's
components, subject to trait implementations and restrictions. It is also
unsafe to implement manually: the implementation must justify every way
transferring the value could affect memory safety. An incorrect unsafe
implementation can allow undefined behavior that safe callers cannot prevent.
Common types that are not Send include:
| Type | Reason |
|---|---|
Rc<T> |
Its shared reference count is not synchronized for concurrent access. |
*const T, *mut T |
Their presence does not establish a safe cross-thread ownership or access contract. |
NonNull<T> |
A non-null pointer alone does not establish that contract either. |
std::sync::MutexGuard<'_, T> |
The standard guard preserves platforms' requirement to unlock on the thread that locked. |
These restrictions do not mean every wrapper containing a raw pointer must
always be non-Send. A carefully implemented abstraction can establish the
necessary invariants and provide its own unsafe implementation. That is
different from adding unsafe impl Send just to bypass this exercise's error.
Choose shared ownership or scoped borrowing
Arc<Vec<String>> supports shared ownership across threads. For the usual
Arc<T> with the default allocator, sending and sharing it requires
T: Send + Sync. Atomic reference counting protects ownership bookkeeping;
it does not make arbitrary operations on T thread-safe.
Arc::clone creates another handle to the same value rather than cloning
the vector's strings. If you take this route, also fix the starter's worker
loop: cap the effective worker count at the nonempty input length, or
otherwise spawn only for nonempty ranges. Replacing Rc with Arc alone
leaves unnecessary threads when workers exceed words.
The reference solution uses thread::scope instead. Workers borrow slices
of words, and the scope joins them before returning. This fits work that
must finish within this call, while the input vector remains alive.
Borrowing avoids an additional reference-counted owner and lets each local
map use &str keys. It does not make the algorithm allocation-free: maps,
handle/result vectors and the final owned strings still need storage, and
spawning threads has costs. Neither approach is universally faster.
Both shared ownership and scoped borrowing can solve the task. Keep the choice tied to the lifetime and ownership requirements of the surrounding program.
Keep borrowed keys alive until conversion
In the scoped solution, each HashMap<&str, usize> refers to strings owned
by the original words vector. Those maps can be returned from their worker
threads because the strings outlive the scope. The vector must stay alive
while the partial maps are merged and their keys are used.
Convert each distinct key to an owned String when building the final
result. The returned vector then owns its strings and does not borrow from
the input that will be dropped when the function returns.
Make the output order explicit
HashMap iteration order is unspecified. Collecting its entries directly would not meet the sorted-output contract. Sort the final pairs by word; each word appears only once, so sorting the pairs also gives the required ordering.
The reference joins handles and merges partial maps in chunk order, not completion order. Thread scheduling can change when work finishes, but it does not change the counts. Sorting removes the remaining map-order variation from the observable output.
Passing requires compilation, correct counts and ordering, and Clippy with warnings treated as errors. Output tests alone cannot prove that an implementation used worker threads.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.