Asynchronous Rust

Polling Futures

Polling is the act of driving a Future forward by invoking Future::poll(self: Pin<&mut Self>, cx: &mut Context<'_>). During a single poll call, a composite future synchronously executes instructions until it either finishes (Poll::Ready(output)) or reaches a child future whose poll returns Poll::Pending.

How Polling Propagates Down a Tree of Futures

When an executor polls a top-level task TaskFuture, the poll call cascades synchronously down to whichever active child future the task is currently .awaiting:

Executor calls `top_task.as_mut().poll(&mut cx)`
       |
       v
  [ Top-Level `handle_request()` Future ]
       | (Currently suspended at `read_packet().await`)
       v
  [ Middle `read_packet()` Future ]
       | (Currently suspended at `socket.read().await`)
       v
  [ Leaf `TcpStreamRead` Future ]
       +-- Calls nonblocking OS `read()`:
             - If `Err(WouldBlock)`: registers `cx.waker()` with reactor -> returns `Poll::Pending`
               (Propagates `Poll::Pending` all the way back to Executor!)
             - If `Ok(n)`: returns `Poll::Ready(Ok(n))` -> `read_packet()` continues immediately!

Polling Two Futures Concurrently (Join2 Combinator)

To run two independent futures concurrently on the same thread (rather than sequentially with a.await; b.await;), a Join2<A, B> combinator stores both futures and polls whichever ones have not yet completed on each poll call, caching each output in an Option until both are Some:

use std::future::Future;
use std::pin::{Pin, pin};
use std::task::{Context, Poll, Waker};

struct Join2<F1: Future, F2: Future> {
    fut1: F1,
    out1: Option<F1::Output>,
    fut2: F2,
    out2: Option<F2::Output>,
}

impl<F1, F2> Future for Join2<F1, F2>
where
    F1: Future,
    F2: Future,
{
    type Output = (F1::Output, F2::Output);

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // SAFETY: We project structural pinning to `fut1` and `fut2` without moving them.
        let this = unsafe { self.get_unchecked_mut() };

        if this.out1.is_none() {
            let pin1 = unsafe { Pin::new_unchecked(&mut this.fut1) };
            if let Poll::Ready(val1) = pin1.poll(cx) {
                this.out1 = Some(val1);
            }
        }

        if this.out2.is_none() {
            let pin2 = unsafe { Pin::new_unchecked(&mut this.fut2) };
            if let Poll::Ready(val2) = pin2.poll(cx) {
                this.out2 = Some(val2);
            }
        }

        if this.out1.is_some() && this.out2.is_some() {
            Poll::Ready((this.out1.take().unwrap(), this.out2.take().unwrap()))
        } else {
            Poll::Pending
        }
    }
}

fn main() {
    let joined = Join2 {
        fut1: async { 10 + 20 },
        out1: None,
        fut2: async { "concurrent" },
        out2: None,
    };

    let mut pinned = pin!(joined);
    let mut cx = Context::from_waker(Waker::noop());
    println!("Join2 poll result: {:?}", pinned.as_mut().poll(&mut cx));
}
Join2 poll result: Ready((30, "concurrent"))

Fused Futures and Post-Completion Polling

Notice in Join2 above how we checked if this.out1.is_none() before polling this.fut1: once fut1 returns Poll::Ready(val1) on an early poll while fut2 is still Poll::Pending, a subsequent poll of Join2 must not call fut1.poll(cx) again, because polling a completed async block panics! Caching the output in Option<Output> (or wrapping a future in a fuse adapter) guarantees each child future is polled at most once after returning Poll::Ready.