Hone logo
Hone
Problems

Building a Minimal Async Runtime in Rust

Asynchronous programming is crucial for building high-performance, I/O-bound applications. Rust's async/await syntax provides a powerful way to write asynchronous code, but it relies on an underlying runtime to manage tasks and schedule their execution. This challenge involves building a simplified asynchronous runtime from scratch in Rust.

Problem Description

Your task is to implement a basic asynchronous runtime that can execute multiple asynchronous tasks concurrently. This runtime should be able to poll futures, wake them up when they are ready, and manage a queue of ready tasks. You will be responsible for the core components that enable async/await to function.

Key requirements:

  • Task Scheduling: Implement a mechanism to schedule and execute asynchronous tasks.
  • Future Polling: The runtime must be able to poll Futures to drive their execution.
  • Waker Management: Implement a way to signal to the runtime that a task is ready to be polled again.
  • Task Queue: Maintain a queue of tasks that are ready to be executed.
  • Spawning Tasks: Provide a function to spawn new asynchronous tasks onto the runtime.
  • Running the Runtime: Implement a method to start the runtime and execute tasks until all are complete.

Expected behavior:

The runtime should accept async functions (which produce Futures) and run them. When a Future is polled and returns Poll::Pending, it should not be dropped immediately. Instead, it should be associated with a Waker that can later signal its readiness. When the Waker is invoked, the task associated with that Waker should be added back to the ready queue for execution.

Examples

Example 1: Simple Task Execution

Let's imagine a simple async function that prints a message and completes.

async fn my_task(id: u32) {
    println!("Task {} started", id);
    // Simulate some work or I/O
    tokio::time::sleep(std::time::Duration::from_millis(10)).await;
    println!("Task {} finished", id);
}

// Inside your runtime's main execution loop:
// let task = my_task(1);
// runtime.spawn(task);

Expected Output (order might vary slightly due to concurrency):

Task 1 started
Task 1 finished

Example 2: Multiple Concurrent Tasks

Consider spawning two tasks that might run interleaved.

async fn print_and_wait(id: u32, duration: std::time::Duration) {
    println!("Task {} starting, will wait for {:?}", id, duration);
    tokio::time::sleep(duration).await;
    println!("Task {} finished after {:?}", id, duration);
}

// Inside your runtime's main execution loop:
// runtime.spawn(print_and_wait(1, std::time::Duration::from_millis(50)));
// runtime.spawn(print_and_wait(2, std::time::Duration::from_millis(20)));

Expected Output (order of starts might vary, but finishes will be ordered):

Task 1 starting, will wait for 50ms
Task 2 starting, will wait for 20ms
Task 2 finished after 20ms
Task 1 finished after 50ms

Example 3: Handling Poll::Pending and Waking

This is a more conceptual example to illustrate the core mechanism. Imagine a future that only completes after being explicitly woken up.

use std::task::{Context, Poll, Waker};
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::thread;

struct MyPendingFuture {
    woken_up: Arc<Mutex<bool>>,
}

impl Future for MyPendingFuture {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut woken_up = self.woken_up.lock().unwrap();
        if *woken_up {
            println!("MyPendingFuture: woken up and completing!");
            Poll::Ready(())
        } else {
            println!("MyPendingFuture: pending, need to be woken up.");
            // Store the waker for later
            let waker = cx.waker().clone();
            // In a real scenario, you'd store this waker and use it when an event occurs.
            // For this example, we'll simulate an external thread waking it.
            // This part would typically be handled by the runtime itself or an external event loop.
            // The important part is that the future *returns Pending* and *can be woken*.
            Poll::Pending
        }
    }
}

// Simulating an external wake-up mechanism
fn trigger_wake(woken_up: Arc<Mutex<bool>>) {
    thread::spawn(move || {
        thread::sleep(Duration::from_millis(30));
        let mut woken_up_guard = woken_up.lock().unwrap();
        *woken_up_guard = true;
        println!("External trigger: setting woken_up to true.");
        // In a real runtime, this would involve calling `waker.wake()`
    });
}

// Inside your runtime's main execution loop:
// let woken_up_flag = Arc::new(Mutex::new(false));
// let future = MyPendingFuture { woken_up: Arc::clone(&woken_up_flag) };
// let task = async {
//     // Need to wrap the future in something that allows it to be polled by the runtime
//     // and have a waker passed in. This is usually handled by the `spawn` mechanism.
//     // For simplicity, imagine `runtime.spawn` takes care of this.
//     // Let's assume a hypothetical `poll_and_manage` function.
//     println!("Spawning MyPendingFuture...");
//     trigger_wake(Arc::clone(&woken_up_flag)); // Simulate external event
//     future.await;
//     println!("MyPendingFuture completed via await.");
// };
// runtime.spawn(task);

Expected Output:

Spawning MyPendingFuture...
MyPendingFuture: pending, need to be woken up.
External trigger: setting woken_up to true.
MyPendingFuture: woken up and completing!
MyPendingFuture completed via await.

Constraints

  • Your runtime should be able to handle at least 100 concurrent tasks.
  • The implementation should primarily use standard Rust library features and std::future. You may use crates for synchronization primitives (like Arc, Mutex, mpsc) but avoid existing async runtimes like tokio, async-std, etc. for the core runtime logic.
  • The runtime should gracefully shut down when all spawned tasks have completed.
  • Error handling for task panics is optional for this challenge but would be a good extension.

Notes

  • The std::task module is your friend. Pay close attention to Future, Context, Poll, and Waker.
  • You will need to manage the lifecycle of futures. When a future returns Poll::Pending, you must keep it around so it can be polled again later.
  • The Waker is the mechanism by which a task signals to the runtime that it's ready to be polled again. You'll need to ensure these Wakers are correctly captured and used.
  • Consider how you will represent a "task" within your runtime. It will likely involve a Pin<Box<dyn Future<Output = ()>>> or similar.
  • Think about how to manage threads if you plan to have a multi-threaded runtime. For a single-threaded runtime, you'll need a way to repeatedly poll ready tasks.
Loading editor...
rust