Hone logo
Hone
Problems

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:

  1. Define a custom Future struct: 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.
  2. Implement the Future trait: For your struct, you must implement the std::future::Future trait, which has a single method: poll.
  3. poll method logic: The poll method should:
    • Check if the asynchronous operation is complete.
    • If complete, return Poll::Ready(value).
    • If not complete, return Poll::Pending and register a waker (explained below) to be notified when the operation is ready.
  4. Waker integration: You need to correctly handle the Waker provided to the poll method. When your operation is ready to make progress (or finish), you must call wake() on the Waker.

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 Future that completes immediately.
  • A Future that requires multiple polls to complete.
  • Handling the Waker correctly when the Future is 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:

  1. The DelayFuture starts with ready: false.
  2. The first call to poll finds self.ready to be false. It marks self.ready as true (simulating that the work will be done by the time it's polled again) and returns Poll::Pending.
  3. The executor, seeing Poll::Pending, would typically wait and then call waker.wake(). This signals the executor to poll the Future again.
  4. The second call to poll finds self.ready to be true, so it returns Poll::Ready(self.value).

Constraints

  • You must use std::future::Future.
  • Your Future should not use any external asynchronous runtimes (like tokio or async-std) for the core Future implementation itself. You can use them conceptually in the examples for demonstration.
  • The poll method should be the only way the Future's state is progressed or observed.
  • Your Future must handle the Waker correctly – although for this simplified challenge, the explicit wake() call might not be necessary to pass if the executor polls repeatedly, understanding its role is key.

Notes

  • The Future trait is designed to be polled repeatedly. An executor is responsible for managing this polling loop.
  • The Waker is the mechanism by which a Future signals to the executor that it is ready to be polled again. When a Future returns Poll::Pending, it must eventually call wake() on the Waker it received in the Context before 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 Pin in self: Pin<&mut Self> is a crucial part of Rust's async story, ensuring that data within a Future doesn't move in memory, which is important for self-referential structs that futures often are. For this challenge, you can often use self.get_mut() to safely access and modify the inner data if your struct is not self-referential.
Loading editor...
rust