Skip to content

← Async From First Principles step 14 of 25

Medium Framework

Join two futures and keep both results

Make the supplied generic Join future compile without restricting its children or their output types to Unpin. Keep the fields, constructor, polling logic, Countdown and race_log unchanged. Add the missing trait implementation and explain why the stored children remain pinned.

pub struct Join<A: Future, B: Future> { /* supplied fields */ }
pub fn race_log(a_pends: u32, b_pends: u32) -> (u32, u32, Vec<char>)

Each supplied Countdown records its tag on every poll. It returns Pending the requested number of times, then Ready on its next poll. The a child returns 10; the b child returns 20. Join waits for both results. Despite its name, race_log records a join, not a race that stops at the first completed child.

Poll unfinished children in a, then b order on each poll of Join. Once a child finishes, retain its result and skip that child on later polls. For example:

race_log(3, 1) = (10, 20, ['a', 'b', 'a', 'b', 'a', 'a'])
race_log(0, 0) = (10, 20, ['a', 'b'])

In the first example, b finishes at the fourth log entry. Only a is polled afterward. The log contains a_pends + 1 copies of 'a' and b_pends + 1 copies of 'b', interleaved in the required order.

Both pending counts are below 100_000, so the supplied driver's polling budget is sufficient. That driver repeatedly polls with a no-op waker; it is a teaching helper for these children, not a general I/O executor.

Preserve a result while the other child waits

ra and rb hold completed outputs. On each poll:

  1. If ra is empty, poll a and save a ready result.
  2. If rb is empty, poll b and save a ready result.
  3. Return both results when both slots are full; otherwise return Pending.

The supplied code takes both slots before matching on them. If only one is full, it restores the slots before returning Pending. Forgetting that restoration would lose a completed result.

Do not poll a completed child again. The general Future contract does not require that to work, even though this particular Countdown returns the same value if polled again. The log also detects those extra polls.

Why the generic implementation fails

The starter needs a mutable reference from Pin<&mut Join<A, B>> at let this = &mut *self. That requires Join<A, B>: Unpin.

The boxed child fields can move without moving their allocations. However, the output slots contain arbitrary A::Output and B::Output values, which need not implement Unpin. Rust therefore cannot derive the required property automatically for every pair of futures.

Add this safe implementation:

impl<A: Future, B: Future> Unpin for Join<A, B> {}

This is valid for the supplied design because Join does not promise to pin its output slots. It owns those outputs as ordinary movable values, even when their types are not Unpin. Only the child futures need stable addresses, and Pin<Box<_>> preserves those addresses when Join moves.

A future version that stores children inline or exposes pinned references to fields needs its pinning guarantees reviewed. In particular, a design cannot promise a field stays pinned and also allow that field to move through an unconditional Unpin implementation. The trait implementation alone is safe syntax; the important question is which pinning guarantees the API makes.

Concurrent progress on one thread

This Join polls its children sequentially within each call, on one thread. Both can make progress before either finishes. It does not create worker threads or make blocking code inside a child's poll nonblocking.

Awaiting one ordinary lazy future and then another polls the first to completion before polling the second. Joining them can overlap waiting, but does not guarantee a particular latency improvement. Futures representing already-spawned tasks can have work progressing independently of these polls.

Both children receive the parent's context. This simple implementation polls every unfinished child whenever the parent is polled; it does not track which child requested a wake. More elaborate combinators can use separate child wake notifications to avoid scanning a large collection.