Hone logo
Hone
Problems

Minimal Tokio Runtime Implementation

This challenge asks you to implement a simplified version of Tokio's runtime, focusing on core concepts like task scheduling and asynchronous execution. Building a basic runtime helps understand how asynchronous Rust works under the hood and provides a foundation for more complex asynchronous systems. The goal is to create a runtime that can execute a few asynchronous tasks concurrently.

Problem Description

You are to implement a minimal Tokio-like runtime in Rust. This runtime should be able to:

  1. Manage Tasks: Maintain a list of asynchronous tasks (represented as Futures).
  2. Execute Tasks: Poll each task in a round-robin fashion until all tasks are completed. A task is considered complete when its poll method returns Poll::Ready.
  3. Handle Poll Results: Correctly handle both Poll::Ready and Poll::Pending results from the Future::poll method. If a task returns Poll::Pending, it should be re-added to the task queue for later execution.
  4. Simple Task Queue: Use a Vec as a simple task queue. While not the most efficient, it's sufficient for this exercise.
  5. No External Dependencies: The solution should not use the actual tokio crate. You are implementing a simplified version from scratch.

Key Requirements:

  • The runtime must implement a run method that takes a vector of Futures as input.
  • The run method should execute all provided futures concurrently.
  • The runtime should block until all futures have completed.
  • The Future trait must be implemented for a simple Counter future (provided below).

Expected Behavior:

The runtime should execute the provided futures, polling them repeatedly until they all return Poll::Ready. The order of execution might vary slightly due to the round-robin scheduling, but all futures should eventually complete.

Edge Cases to Consider:

  • What happens if a future never returns Poll::Ready (an infinite loop)? While a full solution would handle this with timeouts, for this simplified version, you can assume all futures will eventually complete.
  • How does the runtime handle multiple futures returning Poll::Pending in a single iteration? They should be re-added to the queue.

Examples

Example 1:

Input: [Counter::new(3), Counter::new(5)]
Output: ((), ())
Explanation: Two counters are created, one completing after 3 polls, the other after 5. The runtime polls each counter until both are ready.

Example 2:

Input: [Counter::new(1), Counter::new(1)]
Output: ((), ())
Explanation: Two counters, each completing after 1 poll. The runtime polls each counter, and both complete quickly.

Example 3: (Edge Case - demonstrating re-queueing)

Input: [Counter::new(3), Counter::new(1)]
Output: ((), ())
Explanation: The first counter takes 3 polls, the second takes 1. The runtime polls the first, gets Pending, re-queues. Polls the second, gets Ready. Polls the first again, gets Ready.

Constraints

  • The runtime should be implemented using only standard library features and the Future trait.
  • The run method should take a Vec<Box<dyn Future + Send>> as input. The Send bound is necessary for concurrent execution.
  • The runtime should not use any external crates (other than the standard library).
  • Performance is not a primary concern for this exercise; clarity and correctness are more important.
  • The Counter future provided below should be used for testing.

Notes

  • The Future trait is defined as:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
  • Consider using a while loop to continuously poll tasks until all are complete.
  • The Context passed to poll is not used in this simplified implementation, but it's required by the Future trait.
  • The Send trait bound on the futures is important for allowing them to be moved between threads (although this simplified runtime doesn't use threads directly, it's good practice to include it).
  • The Box<dyn Future + Send> allows you to store different types of futures in the same vector.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct Counter {
    count: i32,
}

impl Counter {
    fn new(count: i32) -> Counter {
        Counter { count }
    }
}

impl Future for Counter {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.count > 0 {
            self.count -= 1;
            Poll::Pending
        } else {
            Poll::Ready(())
        }
    }
}

fn main() {}
Loading editor...
rust