Skip to content

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

Easy Primitives

mpsc channels: message passing basics

Repair a function that gathers results from several worker threads:

pub fn fan_in(values: Vec<i64>) -> Vec<i64>

Start one worker per input value. Each worker doubles its value and sends one result through the same channel. Receive exactly values.len() results, join the workers, sort the results in ascending order and return them. Preserve duplicates. Empty input returns an empty vector.

Input Result
[] []
[7] [14]
[3, -1, 3] [-2, 6, 6]

Inputs in this exercise are small enough to use one thread per element, and every doubled value fits in i64. This is a channel exercise, not a thread-pool implementation or an overflow-handling task.

The starter already receives, joins and sorts. Fix how it gives each worker a sender. Keep the counted recv() loop for this exercise.

Clone the handle before moving it into a worker

mpsc means multi-producer, single-consumer. Cloning a Sender creates another handle to the same channel; it does not create a separate queue or duplicate any messages. The receiver does not implement Clone.

The starter moves the original tx into the first worker closure. Another loop iteration would try to use that moved sender, producing E0382. Give each worker its own handle before creating the move closure:

let tx = tx.clone();
handles.push(thread::spawn(move || {
    tx.send(v * 2).expect("receiver is alive");
}));

The inner binding shadows the parent handle for this iteration. The worker owns the clone; the parent still has the original for subsequent clones.

Sending takes a message by value. A non-Copy value such as a String is moved, while the i64 used here is copied. Sending an Arc can also leave shared ownership of its referent, so channels do not inherently eliminate shared state. Here each worker only needs its value and sender handle.

Receive a known number of messages

Different workers may send in different orders. Sorting makes the returned vector independent of that arrival order.

recv() waits when no message is available and a sender is still connected. It returns an error once all senders are gone and queued messages have been drained. Already-buffered messages can still be received after the last sender is dropped.

The counted loop finishes after n successful receives because this task creates n workers, each expected to send once. Receiving those messages and joining the threads serve different purposes: join also waits for each worker to finish and reports a worker panic.

Check your understanding Does keeping the parent's original sender prevent the counted loop from finishing? Show answer

No. The loop stops after the expected number of messages; it does not wait for channel disconnection.

Iterating with for value in rx instead keeps receiving until the channel is disconnected and drained. If the parent retains its sender while waiting, the iterator waits for more messages after the workers finish. Dropping the parent's sender and letting all worker senders drop allows that style of loop to terminate.

Counting is a protocol assumption, not a general guarantee of termination. If a worker fails before sending, the parent can wait indefinitely because it still owns a sender. The later join cannot report that panic while the parent is blocked in recv. Production code needs a failure strategy, such as closing unused senders and handling disconnection, explicit result messages or timeouts.

Related channel operations

  • send returns the unsent message inside SendError<T> if the receiver is gone. A successful send does not guarantee the receiver will process it.
  • try_recv returns immediately. It distinguishes an empty connected channel from a disconnected channel with no remaining messages.
  • recv_timeout waits for at most the requested duration and distinguishes a timeout from disconnection.

Channel handles can cross threads when their message type satisfies the required Send bound. Sending handles and sharing data still have type and lifetime requirements; cloning a sender does not bypass them.

Submissions must compile, pass the cases and pass the Rust Clippy gate.