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.