Understanding Rust's Polling with Custom Futures
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Introduction
Asynchronous programming has become an indispensable paradigm for building performant and responsive applications, especially in domains like networking, I/O-bound tasks, and high-concurrency systems. Rust, with its strong type system and ownership model, offers a powerful and safe approach to async programming built upon its Future
trait. While you often interact with futures through the async/await
syntax, truly comprehending how these abstractions work under the hood is crucial for debugging, optimizing, and even designing custom asynchronous components. This deep dive into writing a custom Future
will demystify the polling mechanism, revealing the fundamental dance between your async tasks and the executor, ultimately empowering you to wield Rust's async capabilities with greater confidence and precision.
The Heart of Asynchronous Execution: Polling
Before we construct our custom future, let's establish a clear understanding of the core concepts involved:
- Future Trait: In Rust, a
Future
is a trait that represents an asynchronous computation that may or may not have completed yet. It has a single method,poll
, which an executor repeatedly calls to check the future's progress. - Executor: An executor is responsible for taking
Future
s and driving them to completion by repeatedly calling theirpoll
method. It manages the lifecycle of futures, schedules tasks, and handles waking up tasks when they are ready to make progress. Popular executors includetokio
andasync-std
. - Polling: This is the act of the executor calling the
poll
method on an uncompletedFuture
. Whenpoll
is called, the future attempts to make progress. Poll
Enum: Thepoll
method returns aPoll
enum, which has two variants:Poll::Ready(T)
: Indicates that the future has completed successfully, andT
is the result of the computation.Poll::Pending
: Indicates that the future is not yet complete. WhenPending
is returned, the future must ensure that it arranges to be woken up (viaWaker
) when it's ready to make further progress.
Waker
: AWaker
is a low-level mechanism provided by the executor to allow aFuture
to signal that it's ready to be polled again. When a future returnsPoll::Pending
, it captures and clones theWaker
from theContext
. Later, when an event occurs that might unblock the future (e.g., data arrives on a socket, a timer expires), the future callswaker.wake_by_ref()
to notify the executor to re-poll it.Context
: TheContext
passed to thepoll
method contains aWaker
and other information useful for the future to interact with the executor.
Building a Custom Future: A Simple Delay
Let's create a custom Future
that introduces a non-blocking delay. This will allow us to observe the polling mechanism directly.
We'll define a Delay
struct that holds a deadline
(when it should complete) and an optional Waker
to wake up the task.
use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll, Waker}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::thread; // Represents the state of our delay future struct Delay { deadline: Instant, // We need to store the waker to wake up the future when the deadline passes. // Arc<Mutex<Option<Waker>>> allows us to share and safely modify the waker across threads. waker_storage: Arc<Mutex<Option<Waker>>>, // A flag to ensure we only spawn the timer thread once. timer_thread_spawned: bool, } impl Delay { fn new(duration: Duration) -> Self { Delay { deadline: Instant::now() + duration, waker_storage: Arc::new(Mutex::new(None)), timer_thread_spawned: false, } } } // Implement the Future trait for our Delay struct impl Future for Delay { // The output type of our future is unit, as it just represents a delay. type Output = (); // The core of the future: the poll method fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { // If the deadline has already passed, the future is ready. if Instant::now() >= self.deadline { println!("Delay future: Deadline reached. Returning Ready."); return Poll::Ready(()); } // --- Store the Waker and Set up the Timer (only once) --- // If the timer thread hasn't been spawned yet, set it up. if !self.timer_thread_spawned { println!("Delay future: First poll. Storing waker and spawning timer thread."); // Store the current waker from the context. // This waker will be used by the timer thread to wake up this task. let mut waker_guard = self.waker_storage.lock().unwrap(); *waker_guard = Some(cx.waker().clone()); drop(waker_guard); // Release the lock early // Clone the Arc to pass it to the new thread. let waker_storage_clone = self.waker_storage.clone(); let duration_until_deadline = self.deadline.duration_since(Instant::now()); // Spawn a new thread that will sleep until the deadline // and then wake up the original task. thread::spawn(move || { thread::sleep(duration_until_deadline); println!("Delay timer thread: Deadline passed. Waking up the task."); // Retrieve the waker and wake the task if let Some(waker) = waker_storage_clone.lock().unwrap().take() { waker.wake(); } }); // Mark that the timer thread is spawned to avoid re-spawning self.timer_thread_spawned = true; } else { // This part handles subsequent polls if the timer thread is already running. // It's crucial to update the waker if the executor decides to move the task // or re-schedule it. If the waker isn't updated, the previous waker might become // stale, leading to the task never being woken up. let mut waker_guard = self.waker_storage.lock().unwrap(); if waker_guard.as_ref().map_or(true, |w| !w.will_wake(cx.waker())) { println!("Delay future: Waker changed. Updating."); *waker_guard = Some(cx.waker().clone()); } } // If the deadline has not passed yet, the future is pending. // It will be re-polled when the `waker.wake()` is called by the timer thread. println!("Delay future: Deadline not yet reached. Returning Pending."); Poll::Pending } } #[tokio::main] async fn main() { println!("Main: Starting program."); let delay_future = Delay::new(Duration::from_secs(2)); // Create a 2-second delay println!("Main: Awaiting delay future..."); delay_future.await; // Await our custom future println!("Main: Delay completed. Program finished."); }
Explanation of the Delay
Future:
-
struct Delay
:deadline
: AnInstant
representing when the delay should finish.waker_storage
: AnArc<Mutex<Option<Waker>>>
is essential. TheWaker
needs to be shared between theFuture
(which ownsself.waker_storage
) and the separatethread::spawn
(which will callwake
).Arc
enables shared ownership, andMutex
provides safe interior mutability to store and retrieve theWaker
.Option
is used because theWaker
might not be available on the very firstpoll
before it's stored.timer_thread_spawned
: A simple boolean flag to ensure we only spawn our "timer" thread once.
-
impl Future for Delay
:type Output = ();
: Our delay future simply completes, producing no meaningful value.poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>
: This is the core.if Instant::now() >= self.deadline
: On every poll, we check if the deadline has passed. If so, we'reReady
and returnPoll::Ready(())
.if !self.timer_thread_spawned
: This conditional block ensures that we only set up the actual timer (thethread::spawn
part) once.let mut waker_guard = self.waker_storage.lock().unwrap(); *waker_guard = Some(cx.waker().clone());
: We acquire a lock on ourwaker_storage
,clone
theWaker
from the currentContext
, and store it. ThisWaker
points back to this specific task that is currently being polled.thread::spawn(...)
: We launch a standard Rust thread. This thread willsleep()
for the remaining duration. This is a blockingsleep
from the perspective of this helper thread, but it doesn't block the executor thread because it's in a separate OS thread.- Inside the spawned thread, after sleeping, it retrieves the stored
Waker
and callswaker.wake()
. Thiswake()
call tells the async runtime (Tokio in ourmain
) that the task associated with thisWaker
is now ready to be re-polled. self.timer_thread_spawned = true;
: We set the flag to true to prevent spawning multiple timer threads for the sameDelay
future.
else { ... }
: If the timer thread has been spawned (i.e., this is a subsequent poll of an already pending future), we still need to check if theWaker
in theContext
has changed (!w.will_wake(cx.waker())
). If it has, we update our storedWaker
. This is important because executors can sometimes move or re-schedule tasks, requiring a newWaker
to correctly notify the task.Poll::Pending
: If the deadline hasn't passed and the timer is set up, the future is still waiting. We returnPoll::Pending
. The executor will stop polling this future untilwaker.wake()
is called.
How it operates with tokio::main
and await
:
Delay::new(Duration::from_secs(2))
: ADelay
instance is created.delay_future.await
: This is where the magic happens.- Tokio's executor receives
delay_future
. - First Poll: The executor calls
delay_future.poll(cx)
.- The deadline is not met.
timer_thread_spawned
isfalse
.- The
Waker
fromcx
is cloned and stored indelay_future.waker_storage
. - A new
thread::spawn
is created. This thread starts sleeping for 2 seconds. timer_thread_spawned
is set totrue
.poll
returnsPoll::Pending
.
- Executor's action after
Poll::Pending
: The executor now knowsdelay_future
is not ready. It puts this task aside and starts polling other ready tasks (if any) or waits forwaker.wake()
calls. Crucially, the Tokio runtime thread is not blocked by ourthread::spawn
'sthread::sleep
. - After 2 seconds: The
thread::spawn
completes itsthread::sleep
.- It retrieves the stored
Waker
and callswaker.wake()
.
- It retrieves the stored
- Executor's action after
waker.wake()
: The executor receives the wake-up signal for the task associated withdelay_future
. It schedulesdelay_future
to be polled again. - Second (or later) Poll: The executor calls
delay_future.poll(cx)
again.- Now,
Instant::now() >= self.deadline
is true. poll
returnsPoll::Ready(())
.
- Now,
- Completion: The
delay_future.await
expression finally completes, and themain
function continues.
- Tokio's executor receives
Conclusion
By implementing a custom Delay
future, we've gained a hands-on understanding of Rust's asynchronous polling mechanism. We've seen how Future::poll
is repeatedly invoked by an executor, how Poll::Pending
signals an incomplete state, and critically, how the Waker
acts as the bridge, allowing an awaiting task to signal the executor to recommence polling when progress can be made. This explicit dance between the Future
and the Executor
via the Waker
is the bedrock of Rust's efficient, non-blocking asynchronous programming, enabling high-performance and scalable applications without the overhead of blocking threads. Mastering custom Future
implementations is an advanced skill that unlocks deeper insights into Rust's powerful async ecosystem.