Definition of Future
The Future
is the core of asynchronous programming in Rust. Here's the definition of the Future
trait:
#[must_use = "futures do nothing unless you `.await` or poll them"]
#[lang = "future_trait"]
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
#[must_use = "this `Poll` may be a `Pending` variant, which should be handled"]
pub enum Poll<T> {
Ready(T),
Pending,
}
A Future
has an associated type Output
, and a method poll()
which returns a Poll<Self::Output>
. Poll
is an enum with two variants: Ready
and Pending
. By calling the poll()
method, a Future
can be progressed further toward completion until the task is done and switched out.
In a current
poll
call, if theFuture
is complete, it returnsPoll::Ready(result)
, which means the value of theFuture
is returned. If theFuture
is not yet complete, it returnsPoll::Pending()
. At this point, theFuture
will be suspended, waiting to be woken up by some event (via a wake function).
Executor (Execution Scheduler)
An executor is a scheduler for a Future
. While the operating system is responsible for scheduling threads, it does not schedule user-space coroutines (like Future
s). Therefore, any program that uses coroutines for concurrency needs an executor to handle scheduling.
Rust's Future
s are lazy—they only run when polled. One way to drive them is to call another async function inside an async function using .await
, but that only solves the issue within async functions themselves. The outermost async functions still need to be driven by an executor.
Executor Runtime
Although Rust provides coroutines like Future
s, it does not provide an executor at the language level. If coroutines are not used, there's no need to bring in any runtime; if they are needed, the ecosystem offers a variety of executors to choose from.
Here are 4 common executors in Rust:
-
futures
: This library comes with a simple built-in executor. -
tokio
: Provides an executor; using#[tokio::main]
implicitly includes Tokio’s executor. -
async-std
: Provides an executor similar to Tokio. -
smol
: Providesasync-executor
, primarily exposingblock_on
.
Wake Notification Mechanism
An executor manages a group of Future
s (typically the outermost async functions), and advances them by continuously polling until they complete. Initially, the executor polls a Future
once. After that, it won't actively poll again. If the poll
method returns Poll::Pending
, the Future
is suspended until some event triggers its wake-up via the wake()
function. The Future
can then actively notify the executor, prompting it to resume polling and continue executing the task. This wake-then-poll cycle repeats until the Future
completes.
The Waker
provides a wake()
method, which tells the executor that the associated task is ready to be resumed, allowing the executor to poll the corresponding Future
again.
Context
is a wrapper around Waker
. Let's look at the Context
structure used in the poll
method:
pub struct Context<'a> {
waker: &'a Waker,
_marker: PhantomData<fn(&'a ()) -> &'a ()>,
}
The definition and implementation of Waker
are quite abstract. Internally, it uses a virtual function table (vtable) to allow for a variety of waker
behaviors:
pub struct RawWakerVTable {
clone: unsafe fn(*const ()) -> RawWaker,
wake: unsafe fn(*const ()),
wake_by_ref: unsafe fn(*const ()),
drop: unsafe fn(*const ()),
}
Rust itself does not provide an async runtime—it only defines basic interfaces in the standard library, leaving runtime behavior to be implemented by third-party runtimes. So in the standard library, you will only see the interface definitions and some high-level implementations. For example, the wake()
method on Waker
simply delegates the call to the corresponding function in the vtable:
impl Waker {
/// Wake up the task associated with this `Waker`.
#[inline]
pub fn wake(self) {
// The actual wakeup call is delegated through a virtual function call
// to the implementation which is defined by the executor.
let wake = self.waker.vtable.wake;
let data = self.waker.data;
// Don't call `drop` -- the waker will be consumed by `wake`.
crate::mem::forget(self);
// SAFETY: This is safe because `Waker::from_raw` is the only way
// to initialize `wake` and `data` requiring the user to acknowledge
// that the contract of `RawWaker` is upheld.
unsafe { (wake)(data) };
}
...
}
The actual implementation of the vtable is not in the standard library—it is provided by third-party async runtimes, such as the one in the futures
crate.
Building a Timer
Let's use a timer example to help understand the scheduling mechanism of a Future
. The goal is: when creating a timer, a new thread is spawned that sleeps for a specific duration, and once the time window has passed, it signals the timer Future
.
Note: This requires the ArcWake
trait from the futures
crate, which provides a convenient way to construct a Waker
. Edit Cargo.toml
and add the following dependency:
[dependencies]
futures = "0.3"
Full code for the timer Future
:
// future_timer.rs
use futures;
use std::{
future::Future,
pin::Pin,
sync::{Arc, Mutex},
task::{Context, Poll, Waker},
thread,
time::Duration,
};
pub struct TimerFuture {
shared_state: Arc<Mutex<SharedState>>,
}
/// Shared state between the Future and the sleeping thread
struct SharedState {
/// Indicates whether the timer (sleep) has completed
completed: bool,
/// When sleep ends, the thread can use this `waker` to notify `TimerFuture` to wake up the task
waker: Option<Waker>,
}
impl Future for TimerFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// Check shared state to determine if the timer has completed
let mut shared_state = self.shared_state.lock().unwrap();
if shared_state.completed {
println!("future ready. execute poll to return.");
Poll::Ready(())
} else {
println!("future not ready, tell the future task how to wakeup to executor");
// Set the `waker` so that the new thread can wake up the task once the sleep completes,
// allowing the `Future` to be polled again.
// The `clone` here happens on every `poll`, but ideally should only happen once.
// The reason we clone each time is because the `TimerFuture` might move between tasks
// in the executor; a single `waker` instance might be altered and point to the wrong task,
// resulting in the executor running the wrong task.
shared_state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}
impl TimerFuture {
/// Create a new `TimerFuture` which completes after the specified duration
pub fn new(duration: Duration) -> Self {
let shared_state = Arc::new(Mutex::new(SharedState {
completed: false,
waker: None,
}));
// Spawn a new thread
let thread_shared_state = shared_state.clone();
thread::spawn(move || {
// Sleep for the specified duration to simulate a timer
thread::sleep(duration);
let mut shared_state = thread_shared_state.lock().unwrap();
// Notify the executor that the timer is done and the corresponding `Future` can be polled again
shared_state.completed = true;
if let Some(waker) = shared_state.waker.take() {
println!("detect future is ready, wakeup the future task to executor.");
waker.wake()
}
});
TimerFuture { shared_state }
}
}
fn main() {
// We haven't implemented our own executor yet, so we use the one from the `futures` crate
futures::executor::block_on(TimerFuture::new(Duration::new(10, 0)));
}
Execution result:
future not ready, tell the future task how to wakeup to executor
detect future is ready, wakeup the future task to executor.
future ready. execute poll to return.
As shown above, at first the 10-second timer hasn't completed and is in a Pending
state. At this point, we need to tell the task how to wake itself up when it becomes ready. After 10 seconds, the timer completes and uses the previously set Waker
to wake the Future
task for execution.
Building an Executor
In the previous code, we did not implement our own scheduler—we used the executor provided by the futures
crate. Now, let's build a custom executor ourselves to understand how it works under the hood. However, in real-world Rust async programming, you would typically use the tokio
library. Here, we're building one from scratch for learning purposes, to better understand how async works.
Key code:
// future_executor.rs
use {
futures::{
future::{BoxFuture, FutureExt},
task::{waker_ref, ArcWake},
},
std::{
future::Future,
sync::mpsc::{sync_channel, Receiver, SyncSender},
sync::{Arc, Mutex},
task::Context,
time::Duration,
},
};
mod future_timer;
// Import the previously implemented timer module
use future_timer::TimerFuture;
/// Task executor: responsible for receiving tasks from a channel and executing them
struct Executor {
ready_queue: Receiver<Arc<Task>>,
}
/// `Spawner` is responsible for creating new `Future`s and sending them to the task channel
#[derive(Clone)]
struct Spawner {
task_sender: SyncSender<Arc<Task>>,
}
/// A `Future` that can schedule itself (by sending itself to the task channel), and wait to be executed
struct Task {
/// The in-progress `Future` that will complete at some point in the future
///
/// Technically, a `Mutex` here is unnecessary because we're executing everything on a single thread.
/// But Rust isn't smart enough to know that the `Future` is not being shared across threads,
/// so we use `Mutex` to satisfy the compiler's requirements for thread safety.
///
/// A production-grade executor wouldn't use a `Mutex` here because it introduces overhead.
/// Instead, it would use `UnsafeCell`.
future: Mutex<Option<BoxFuture<'static, ()>>>,
/// Allows this task to re-submit itself into the task queue, waiting for the executor to `poll` it
task_sender: SyncSender<Arc<Task>>,
}
fn new_executor_and_spawner() -> (Executor, Spawner) {
// Maximum number of buffered tasks in the task channel (queue length)
// This implementation is simplified; real-world executors handle this differently
const MAX_QUEUED_TASKS: usize = 10_000;
let (task_sender, ready_queue) = sync_channel(MAX_QUEUED_TASKS);
(Executor { ready_queue }, Spawner { task_sender })
}
impl Spawner {
fn spawn(&self, future: impl Future<Output = ()> + 'static + Send) {
let future = future.boxed();
let task = Arc::new(Task {
future: Mutex::new(Some(future)),
task_sender: self.task_sender.clone(),
});
println!("first dispatch the future task to executor.");
self.task_sender.send(task).expect("too many tasks queued.");
}
}
/// Implements `ArcWake` to define how to wake a task and schedule it for execution
impl ArcWake for Task {
fn wake_by_ref(arc_self: &Arc<Self>) {
// Wake is implemented by sending the task back into the task channel,
// so the executor will `poll` it again.
let cloned = arc_self.clone();
arc_self
.task_sender
.send(cloned)
.expect("too many tasks queued");
}
}
impl Executor {
/// Actually run the `Future` tasks by continuously receiving and executing them
fn run(&self) {
let mut count = 0;
while let Ok(task) = self.ready_queue.recv() {
count += 1;
println!("received task. {}", count);
// Retrieve the future; if it hasn't finished (still Some), then `poll` it and try to complete it
let mut future_slot = task.future.lock().unwrap();
if let Some(mut future) = future_slot.take() {
// Create a `LocalWaker` based on the task itself
let waker = waker_ref(&task);
let context = &mut Context::from_waker(&*waker);
// `BoxFuture<T>` is an alias for `Pin<Box<dyn Future<Output = T> + Send + 'static>>`
// `as_mut` converts it to `Pin<&mut dyn Future + Send + 'static>`
if future.as_mut().poll(context).is_pending() {
println!("executor run the future task, but is not ready, create a future again.");
// Future is not yet done, so put it back and wait for the next poll
*future_slot = Some(future);
} else {
println!("executor run the future task, is ready. the future task is done.");
}
}
}
}
}
fn main() {
let (executor, spawner) = new_executor_and_spawner();
// Wrap the TimerFuture in a task and dispatch it to the scheduler for execution
spawner.spawn(async {
println!("TimerFuture await");
// Create a timer Future and wait for it to complete
TimerFuture::new(Duration::new(10, 0)).await;
println!("TimerFuture Done");
});
// Drop the spawner so the executor knows no more tasks will be submitted
drop(spawner);
// Run the executor until the task queue is empty
// Once the task runs, it will print "howdy!", pause for 2 seconds, then print "done!"
executor.run();
}
Execution result:
first dispatch the future task to executor.
received task. 1
TimerFuture await
future not ready, tell the future task how to wakeup to executor
executor run the future task, but is not ready, create a future again.
detect future is ready, wakeup the future task to executor.
received task. 2
future ready. execute poll to return.
TimerFuture Done
executor run the future task, is ready. the future task is done.
On the first scheduling attempt, the task is not yet ready and returns Pending
. The task is then informed how it should be woken up when it becomes ready. Later, when the event is ready, the task is woken up as instructed and scheduled for execution.
Asynchronous Processing Flow
The Reactor Pattern is a classic design pattern used to build high-performance event-driven systems. The executor and reactor are components of this pattern. The Reactor Pattern is composed of three main parts:
- Task: A unit of work to be executed. Tasks can be paused and yield control to the executor, waiting to be rescheduled later.
- Executor: A scheduler that maintains tasks ready to run (the ready queue) and tasks that are blocked (the wait queue).
- Reactor: Maintains an event queue. When an event occurs, it notifies the executor to wake a specific task to be run.
The executor schedules tasks for execution. When a task cannot proceed but is not yet complete, it will be suspended, and a proper wake-up condition will be registered. Later, if the reactor receives an event that satisfies the wake-up condition, it wakes the suspended task. The executor can then resume polling this task. This cycle repeats until the task is complete.
Rust’s asynchronous handling via Future
is a typical implementation of the Reactor Pattern.
Taking
tokio
as an example:
async/await
provides syntax-level support, andFuture
is the data structure representing asynchronous tasks. When.await
is called, the executor will schedule and execute the task.Tokio’s scheduler runs across multiple threads. Each thread runs tasks from its own ready queue. If a thread’s queue is empty, it can steal tasks from other threads' queues (a strategy called work-stealing).
When a task can no longer make progress and returnsPoll::Pending
, the scheduler suspends it and sets an appropriate wake-up condition using aWaker
. The reactor uses the OS’s async I/O mechanisms (such asepoll
,kqueue
, orIOCP
) to monitor I/O events. When a relevant event is triggered, the reactor callsWaker::wake()
, which wakes the suspendedFuture
. TheFuture
is placed back into the ready queue and awaits execution.
Summary
Future
is the core abstraction in Rust’s asynchronous programming model, representing operations that will complete at some point in the future. Rust’s Future
s are lazy—they require an executor to drive them. This execution is implemented through polling:
- In a current poll cycle, if a
Future
is complete, it returnsPoll::Ready(result)
, providing the final value. - If the
Future
is not yet complete, it returnsPoll::Pending()
. At this point, theFuture
is suspended and awaits an external event to wake it up via aWaker
.
The Waker
provides a wake()
method to notify the executor which task should be resumed. When wake()
is called, the executor knows that the task associated with the Waker
is ready to make progress and polls the Future
again. This wake → poll → suspend cycle continues until the Future
is finally completed.
Each asynchronous task generally goes through three stages:
-
Polling Phase: The executor initiates polling on a
Future
. If it hits a point where it can't make further progress (Poll::Pending
), the task is suspended and enters the waiting phase. -
Waiting Phase: The event source (typically called a reactor) registers a
Waker
to wait for an event. When the event occurs, it triggers theWaker
to wake the associatedFuture
, transitioning to the wake-up phase. -
Wake-Up Phase: When the event occurs, the corresponding
Future
is woken by itsWaker
. The executor schedules theFuture
for polling again. The task progresses until it either completes or reaches anotherPending
point. This cycle repeats until the task is fully completed.
We are Leapcell, your top choice for hosting Rust projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ
Top comments (0)