We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 17 of 24
The drop(tx) hang: a bug with no diagnostics
Square every value across workers threads and collect the results, sorted.
pub fn drain_all(values: Vec<i64>, workers: usize) -> Vec<i64>
You must collect with for x in rx — the iterator form, not a counted
recv() loop. Treat workers == 0 as 1. Empty input gives [].
That single requirement is the entire problem. It forces you to get channel shutdown right, and channel shutdown in Rust is expressed through ownership.
The starter compiles clean, passes clippy clean, and hangs forever
Read that again, because it is the reason this item exists.
$ cargo clippy -- -D warnings
Finished in 0.31s
$ cargo run
(nothing. ever.)
No error. No warning. No output. Not even on the empty input, which is the case you would swear could not possibly break. This is the most common std-channel bug there is, and the toolchain has nothing whatsoever to say about it.
Why
for x in rx desugars to repeated rx.recv(), stopping when recv() returns
Err(RecvError). And recv() returns Err under exactly one condition:
every Sender has been dropped.
Count the senders in the starter:
- one clone moved into each worker — those drop when the workers finish;
-
the original
tx, still owned by the parent thread, alive for the whole function.
So the channel is never disconnected. The loop drains every message and then
blocks on the next recv() forever, waiting for a sender that will never
send — and it is the very thread that is blocked which owns it.
The empty-input case is the cruellest and the most instructive. There are no
workers at all, so there are no clones, and the parent’s tx is the only
sender. Zero messages, one live sender, infinite wait. The input a reviewer
would skip as trivial is the one that hangs first.
Three fixes, all about ownership
drop(tx); // explicit, and says exactly what it means
after the spawn loop. Or move the original into the last worker instead of cloning for it. Or create the senders inside a block so the original goes out of scope before you start draining.
The explicit drop is the clearest, and it deserves a comment: drop(tx) on
its own looks like a no-op to anyone who has not been bitten. Write down that
the receiver’s termination depends on it.
The design point underneath
A channel closes when nobody can write to it any more, and Rust knows that
because it knows who owns what. There is no close() method, and the absence
is deliberate: an explicit close() invites the questions “what if I send
after closing?” and “what if two threads close?”, neither of which can arise
here.
The cost is that “who still holds a Sender?” becomes a design question you
have to be able to answer. In a worker pool, senders are held by workers and
the pool must not keep one. In a pipeline, each stage drops its outbound
sender when its inbound receiver ends, and that is what makes shutdown
cascade. Draw the ownership, and shutdown falls out.
::: question You add a let keep_alive = tx.clone(); for debugging and forget to remove it. What happens?
The program hangs, and every test that passed yesterday now times out with no
explanation.
It is worth internalising how fragile this is: one extra live Sender
anywhere — in a struct field, in a closure that has not been dropped, in a
Vec you kept “just in case” — is enough. And because the failure is a hang
rather than a panic, there is no stack trace, no message, and nothing to
grep for.
If a channel-based program hangs, the first question is always: who still
owns a Sender?
:::
If the grader times out
It is not slow. It is blocked in recv(). Go and count the senders.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.