Skip to content

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

Hard Framework

park and unpark: building a blocking primitive by hand

Repair a relay that passes a turn around a ring of worker threads.

pub fn relay(rounds: usize, threads: usize) -> Vec<usize>

Use t = threads.max(1): a request for zero threads uses one worker. Worker IDs are 0..t. Worker 0 goes first, followed by 1, and so on, wrapping back to 0. Each turn appends the worker's ID to a shared log.

Return the log after exactly rounds * t turns, and join every worker before returning. Zero rounds produce an empty log. Inputs are small enough to create the workers and log, and rounds * t fits in usize.

relay(3, 2) -> [0, 1, 0, 1, 0, 1]
relay(2, 4) -> [0, 1, 2, 3, 0, 1, 2, 3]
relay(3, 1) -> [0, 0, 0]
relay(2, 0) -> [0, 0]
relay(0, 4) -> []

Keep the turn in shared state

Use thread::park and Thread::unpark to wait and signal, with a Mutex protecting the relay state. Do not use a Condvar or replace waiting with a busy loop. These are implementation requirements for the exercise; matching the output alone does not establish that a solution follows them.

The holder field says whose turn it is. A worker must check that field under the mutex before appending its ID. A ring of threads alone does not guarantee output order: the shared turn check and update do that.

Understand the parking token

Each thread has at most one parking token, initially absent. unpark makes it available. park can consume an available token and return immediately, or wait for one. It can also return spuriously, without a token.

Two calls to unpark do not accumulate two tokens. After every return from park, check the shared state again; waking does not mean it is your turn.

If a handoff calls unpark after you unlock but before you call park, the token remains available for that call. Keep this interval free of unrelated code that might itself park and consume the token, including logging callbacks.

When park consumes a token, it synchronizes with the preceding unpark operations for that thread. A spurious return alone provides no such signal. In this relay, the mutex also synchronizes access to the shared fields.

Release the mutex before waiting

The starter calls park while still holding its mutex guard in two places: while waiting for startup and while waiting for its turn.

This can deadlock. For example, a worker can acquire the mutex before the parent publishes the peer handles, then park while holding it. The parent needs that same mutex to finish startup, so it cannot reach its wakeup calls. Whether this happens depends on scheduling; successful compilation does not rule it out.

In both waiting branches, use this sequence:

lock the state
inspect startup, completion, and turn conditions
if waiting is necessary:
    release the guard
    park
    repeat the checks

Do not simply move park outside the lock and then assume the condition is true when it returns. Repeat the loop so spurious or earlier notifications cannot let the wrong worker record a turn.

Publish handles before starting

The parent can obtain a Thread handle from each JoinHandle using handle.thread().clone(). The clone is another handle to the same worker; it does not create a thread.

After spawning the workers, the parent locks the state, installs all peer handles, and sets started = true. It then releases the guard and unparks the workers. No worker may relay before started is true, because handing off requires the peer list to be ready.

Wake waiting workers at completion

A normal turn appends the current ID, increments hops, and sets the next holder under the lock. Release the guard before notifying the next worker.

On the final turn, notify every worker so each can observe completion and exit. Otherwise a worker may remain parked while the parent waits in join. Check completion before waiting for another turn. With zero rounds, workers should see completion immediately after startup.

To investigate a hang, check both waiting branches, startup publication, and the final wakeup of all workers. Repeated runs can expose scheduling bugs, but passing them is not a proof that every possible interleaving is correct.

A passing submission must compile, pass the tests, and pass clippy -D warnings.