Skip to content

← Async From First Principles step 13 of 25

Easy Primitives

poll_fn: a future from a closure

Use std::future::poll_fn to build a future whose state changes once per poll. It wraps a closure: each call to the future's poll method calls that closure with the current Context, then returns its Poll result.

Implement yield_n and keep the supplied drive_all driver:

  • Creating yield_n(n, log) must not change the log.
  • On each poll, append that future's 1-based poll number.
  • For the first n polls, call cx.waker().wake_by_ref() and return Poll::Pending.
  • On poll n + 1, return Poll::Ready(n + 1). No wake is needed then.

Each future starts its own counter at zero. Inputs satisfy 0 <= n < 100_000, so the supplied driver can finish within its poll budget. For n = 0, the first poll logs 1 and completes immediately. The driver stops polling a future once it returns Ready.

drive_all(script) runs one future per entry, sequentially, sharing the same log. It returns (log, results). For [2, 0, 1], the log is [1, 2, 3, 1, 1, 2] and the results are [3, 1, 2]. An empty script produces two empty vectors.

Keeping state between polls

The closure passed to poll_fn implements FnMut, so it can update captured counters. A move closure owns those counters and the supplied Rc handle to the shared log. Store the counters outside the closure body; initializing them inside would reset them on every poll.

Borrow the log only while appending the current count. The temporary RefMut guard from log.borrow_mut().push(count); is released at the end of that statement.

Why the starter finishes too soon

The starter's async block has no suspension point. Its first poll appends one entry and returns, even when n is greater than zero. Adding a loop inside that block would still do all the work during one poll unless it awaited something that returned Pending.

Clippy also reports manual_async_fn for the starter's function returning only an async block. Changing it to async fn addresses that lint but leaves the polling behavior unchanged. Returning a poll_fn future lets you implement the required behavior directly and avoids that lint.

An async function can await a poll_fn future too. The reason to use poll_fn here is that its closure runs once per poll, making the counter and return value explicit.

Wakeups and the supplied driver

This countdown can make progress on the next poll without waiting for an external event, so it requests another poll before returning Pending. A wake requests scheduling; it does not itself call poll.

For real I/O, arrange a wake when progress becomes possible. Repeatedly waking a task while no progress is possible wastes CPU. Wrapping a try_recv call is not sufficient by itself: the future also needs a way to be notified when a message arrives.

The supplied block_on is a limited teaching driver. It polls in a tight loop with a no-op waker and panics after 100,000 unsuccessful polls. It does not wait for notifications. Consequently, the output tests cannot verify that you requested wakeups; follow the polling contract above even though this driver keeps polling without them.

Two related helpers are std::future::ready(value), which completes on its first poll, and std::future::pending::<T>(), which never completes. Passing pending to this driver reaches the panic rather than waiting indefinitely.