Implementing a Basic Asynchronous Task in Rust
This challenge will guide you through implementing a fundamental building block of asynchronous programming in Rust: a custom Future. You will create a Future that represents a task that takes a certain amount of time to complete and can be polled until it's ready. This exercise is crucial for understanding how Rust's async/await syntax works under the hood.
Problem Description
Your goal is to implement a Future trait that simulates an asynchronous operation. This Future will represent a task that produces a specific value after a delay. You'll need to define the structure of your custom Future and its polling mechanism to interact with an executor.
Key Requirements:
- Define a custom
Futurestruct: This struct will hold the state of your asynchronous operation. It should contain the data it will eventually produce and any necessary information to track its progress. - Implement the
Futuretrait: For your struct, you must implement thestd::future::Futuretrait, which has a single method:poll. pollmethod logic: Thepollmethod should:- Check if the asynchronous operation is complete.
- If complete, return
Poll::Ready(value). - If not complete, return
Poll::Pendingand register a waker (explained below) to be notified when the operation is ready.
Wakerintegration: You need to correctly handle theWakerprovided to thepollmethod. When your operation is ready to make progress (or finish), you must callwake()on theWaker.
Expected Behavior:
When a Future is polled, it should either complete immediately, or indicate that it's not yet ready (Poll::Pending). When Poll::Pending is returned, the executor will eventually call wake() on the provided Waker to signal that the Future should be polled again. This process continues until the Future returns Poll::Ready.
Edge Cases:
- A
Futurethat completes immediately. - A
Futurethat requires multiple polls to complete. - Handling the
Wakercorrectly when theFutureis not ready.
Examples
Let's consider a Future that simply holds a value and completes after a simulated delay. For simplicity, we won't actually implement a timer; instead, we'll have a flag that indicates completion.
Example 1: Completing Immediately
Imagine a Future that is already done when it's created.
// Assume a simple executor that repeatedly polls a Future.
// In a real scenario, this would be a complex piece of runtime.
struct ReadyFuture {
value: i32,
completed: bool,
}
impl ReadyFuture {
fn new(value: i32) -> Self {
ReadyFuture { value, completed: true } // Immediately ready
}
}
impl std::future::Future for ReadyFuture {
type Output = i32;
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
if self.completed {
std::task::Poll::Ready(self.value)
} else {
// In a real scenario, you'd register the waker here if not completed.
// For this simple example, we assume it's always completed.
std::task::Poll::Pending
}
}
}
// --- How an executor would interact (simplified conceptual view) ---
// let mut future = ReadyFuture::new(42);
// let mut waker = /* some waker from the executor */;
// let mut context = std::task::Context::from_waker(&waker);
//
// let result = std::future::poll_fn(|cx| future.poll(cx)).await; // This is how await uses poll
//
// // The actual polling loop would look something like:
// loop {
// match std::future::poll_fn(|cx| future.poll(cx)) {
// std::task::Poll::Ready(value) => {
// println!("Future completed with: {}", value); // Output: Future completed with: 42
// break;
// }
// std::task::Poll::Pending => {
// // Executor would wait or do other work, and eventually call waker.wake()
// // For this example, it would loop infinitely if not ready.
// // But since ReadyFuture is always ready, it will complete on first poll.
// }
// }
// }
Explanation:
The ReadyFuture is initialized with completed: true. When poll is called, it checks self.completed. Since it's true, it immediately returns Poll::Ready(self.value).
Example 2: Completing After One Poll
Let's design a Future that needs one poll to become ready.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
use std::sync::{Arc, Mutex};
// A simple struct to track the completion status and hold the value.
struct DelayFuture {
value: i32,
ready: bool,
// In a real implementation, this would involve timers and channels.
// For this exercise, we simulate progress by a boolean flag.
}
impl DelayFuture {
fn new(value: i32) -> Self {
DelayFuture { value, ready: false }
}
}
impl Future for DelayFuture {
type Output = i32;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.ready {
Poll::Ready(self.value)
} else {
// This is where the "work" would happen.
// For this example, we'll just mark it as ready on the *next* poll.
// In a real scenario, you'd schedule a wake-up timer here.
// When the timer fires, it would call cx.waker().wake().
// Simulate work being done:
self.get_mut().ready = true; // Mark as ready for the *next* poll
// Crucially, we *don't* call wake() here immediately.
// The executor will poll us again.
// If we *were* to complete some work that makes it ready,
// we would have scheduled a wake-up.
// For this simplified example, we just return Pending and
// let the executor poll again, where `self.ready` will now be true.
Poll::Pending
}
}
}
// --- How an executor would interact (conceptual view) ---
// let mut future = DelayFuture::new(100);
// let mut waker = /* some waker from the executor */;
// let mut context = std::task::Context::from_waker(&waker);
//
// let mut first_poll = true;
// loop {
// match std::future::poll_fn(|cx| future.poll(cx)) {
// std::task::Poll::Ready(value) => {
// println!("Future completed with: {}", value); // Output: Future completed with: 100
// break;
// }
// std::task::Poll::Pending => {
// if first_poll {
// println!("Future is pending, will complete on next poll."); // Output: Future is pending, will complete on next poll.
// first_poll = false;
// // The executor would now wait, and if something changed (like a timer firing),
// // it would call waker.wake() at some point.
// // For our DelayFuture, it's guaranteed to be ready on the next poll.
// // A real executor would then re-poll this future.
// } else {
// // If it was still pending after the first poll and we didn't wake it,
// // this would loop infinitely.
// // In a real scenario, the `waker.wake()` must have been called.
// }
// }
// }
// }
Explanation:
- The
DelayFuturestarts withready: false. - The first call to
pollfindsself.readyto be false. It marksself.readyastrue(simulating that the work will be done by the time it's polled again) and returnsPoll::Pending. - The executor, seeing
Poll::Pending, would typically wait and then callwaker.wake(). This signals the executor to poll theFutureagain. - The second call to
pollfindsself.readyto betrue, so it returnsPoll::Ready(self.value).
Constraints
- You must use
std::future::Future. - Your
Futureshould not use any external asynchronous runtimes (liketokioorasync-std) for the coreFutureimplementation itself. You can use them conceptually in the examples for demonstration. - The
pollmethod should be the only way theFuture's state is progressed or observed. - Your
Futuremust handle theWakercorrectly – although for this simplified challenge, the explicitwake()call might not be necessary to pass if the executor polls repeatedly, understanding its role is key.
Notes
- The
Futuretrait is designed to be polled repeatedly. An executor is responsible for managing this polling loop. - The
Wakeris the mechanism by which aFuturesignals to the executor that it is ready to be polled again. When aFuturereturnsPoll::Pending, it must eventually callwake()on theWakerit received in theContextbefore it becomes ready. - For this challenge, you can simplify the "work" by using a boolean flag to indicate completion. Real-world futures would involve I/O operations, timers, or other asynchronous events.
- The
Pininself: Pin<&mut Self>is a crucial part of Rust's async story, ensuring that data within aFuturedoesn't move in memory, which is important for self-referential structs that futures often are. For this challenge, you can often useself.get_mut()to safely access and modify the inner data if your struct is not self-referential.