From 326a35939ac2ce0e80b7cbbd35bc6837e3ab5076 Mon Sep 17 00:00:00 2001 From: Philipp Oppermann Date: Tue, 10 Mar 2020 15:43:13 +0100 Subject: [PATCH] Start implementation section --- .../posts/12-async-await/index.md | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/blog/content/second-edition/posts/12-async-await/index.md b/blog/content/second-edition/posts/12-async-await/index.md index 482b2ee7..6de2d9e4 100644 --- a/blog/content/second-edition/posts/12-async-await/index.md +++ b/blog/content/second-edition/posts/12-async-await/index.md @@ -766,5 +766,190 @@ We see that futures and async/await fit the cooperative multitasking pattern per ## Implementation +Now that we understand how cooperative multitasking based on futures and async/await works in Rust, it's time to add support for it to our kernel. Since the [`Future`] trait is part of the `core` library and async/await is a feature of the language itself, there is nothing special we need to do to use it in our `#![no_std]` kernel. The only requirement is that we use at least nightly-TODO of Rust because async/await was based on parts of the standard library before. +With a recent-enough nightly, we can start using async/await in our `main.rs`: + +```rust +// in src/main.rs + +async fn async_number() -> u32 { + 42 +} + +async fn example_task() { + let number = async_number().await; + println!("async number: {}", number); +} +``` + +The `async_number` function is an `async fn`, so the compiler transforms it into a state machine that implements `Future`. Since the function only returns `42`, the resulting future will directly return `Poll::Ready(42)` on the first `poll` call. Like `async_number`, the `example_task` function is also an `async fn`. It awaits the number returned by `async_number` and then prints it using the `println` macro. + +To run the future returned by `example_task`, we need to call `poll` on it until it signals its completion by returning `Poll::Ready`. To do this, we need to create a simple executor type. + +### Task + +Before we start the executor implementation, we create a new `task` module with a `Task` type: + +```rust +// in src/lib.rs + +pub mod task; +``` + +```rust +// in src/task/mod.rs + +pub struct Task { + future: Pin>>, +} +``` + +The `Task` struct is a newtype wrapper around a pinned, heap allocated, dynamically dispatched future with the empty type `()` as output. Let's go through it in detail: + +- We require that the future associated with a task returns `()`. So tasks don't return any result, they are just executed for its side effects. For example, the `example_task` function we defined above has no return value, but it prints something to the screen as a side effect. +- The `dyn` keyword indicates that we store a [trait object] in the `Box`. This means that the type of the future is [dynamically dispatched], which makes it possible to store different types of futures in the task. This is important because each `async fn` has their own type and we want to be able to create different tasks later. +- As we learned in the [section about pinning], the `Pin` type ensures that a value cannot be moved in memory by placing it on the heap and preventing the creation of `&mut` references to it. This is important because futures generated by async/await might be self-referential, i.e. contain pointers to itself that would be invalidated when the future is moved. + +[trait object]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html +[dynamically dispatched]: https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch +[section about pinning]: #pinning + +To allow the creation of new `Task` structs from futures, we create a `new` function: + +```rust +// in src/task/mod.rs + +impl Task { + pub fn new(future: impl Future) -> Task { + Task { + future: Box::pin(future), + } + } +} +``` + +The function takes an arbitrary future with output type `()` and pins it in memory through the [`Box::pin`] function. Then it wraps it in the `Task` struct and returns the new task. + +We also add a `poll` method to allow the executor to poll the corresponding future: + +```rust +// in src/task/mod.rs + +impl Task { + fn poll(&mut self, context: &mut Context) -> Poll { + self.future.as_mut().poll(context) + } +} +``` + +Since the [`poll`] method of the `Future` trait expects to be called on a `Pin<&mut T>` type, we use the [`Pin::as_mut`] method to convert the `self.future` field of type `Pin>` first. Then we call `poll` on the converted `self.future` field and return the result. Since the `Task::poll` method should be only called by the executor that we create in a moment, we keep the function private to the `task` module. + +### Simple Executor + +Since executors can be quite complex, we deliberately start with creating a very basic executor before we implement a more featureful executor later. For this, we first create a new `task::simple_executor` submodule: + +```rust +// in src/task/mod.rs + +pub mod simple_executor; +``` + +```rust +// in src/task/simple_executor.rs + +use super::Task; +use alloc::collections::VecDeque; + +pub struct SimpleExecutor { + task_queue: VecDeque, +} + +impl SimpleExecutor { + pub fn new() -> SimpleExecutor { + SimpleExecutor { + task_queue:: VecDeque::new(), + } + } + + pub fn spawn(&mut self, task: Task) { + self.task_queue.push_back(task) + } +} +``` + +The struct contains a single `task_queue` field of type [`VecDeque`], which is basically a vector that allows to push and pop on both ends. The idea behind using this type is that we insert new tasks through the `spawn` method at the end and pop the next task for execution from the front. This way, we get a simple [FIFO queue] (_"first in, first out"_). + +[`VecDeque`]: https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html +[FIFO queue]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics) + +#### Dummy Waker + +In order to call the `poll` method, we need to create a [`Context`] type, which wraps a [`Waker`] type. To start simple, we will first create a dummy waker that does nothing. The simplest way to do this is by implementing the [`Wake`] trait: + +[`Wake`]: https://doc.rust-lang.org/nightly/alloc/task/trait.Wake.html + +```rust +// in src/task/simple_executor.rs + +use alloc::task::Wake; + +struct DummyWaker; + +impl Wake for DummyWaker { + fn wake(self: Arc) { + // do nothing + } +} +``` + +Since the [`Waker`] type implements the [`From>`] trait for all types `W` that implement the [`Wake`] trait, we can easily create a `Waker` through `Waker::from(DummyWaker)`. We will utilize this in the following to create a simple `Executor::run` method. + +[`From>`]: TODO + +#### A `run` Method + +The most simple `run` method is to repeatedly poll all queued tasks in a loop until all are done. This is not very efficient since it does not utilize the notifications of the `Waker` type, but it is an easy way to get things running: + +```rust +// in src/task/simple_executor.rs + +impl SimpleExecutor { + pub fn run(&mut self) { + while let Some(mut task) = self.task_queue.pop_front() { + let mut context = Context::from_waker(Waker::from(DummyWaker)); + match task.poll(&mut context) { + Poll::Ready(()) => {} // task done + Poll::Pending => self.task_queue.push_back(task), + } + } + } +} +``` + +The function uses a `while let` loop to handle all tasks in the `task_queue`. For each task, it first creates a `Context` type by wrapping a `Waker` instance created from our `DummyWaker` type. Then it invokes the `Task::poll` method with this `Context`. If the `poll` method returns `Poll::Ready`, the task is finished and we can continue with the next task. If the task is still `Poll::Pending`, we add it to the back of the queue again so that it will be polled again in a subsequent loop iteration. + +#### Trying It + +With our `SimpleExecutor` type, we can now try running the task returned by the `example_task` function in our `main.rs`: + +```rust +// in src/main.rs + +use blog_os::task::{Task, simple_executor::SimpleExecutor}; + +fn kernel_main(boot_info: &'static BootInfo) -> ! { + // […] initialization routines, including `init_heap` + + let mut executor = SimpleExecutor::new(); + executor.spawn(Task::new(example_task())): + executor.run(); + + // […] test_main, "it did not crash" message, hlt_loop +} +``` + +When we run it, we see that the expected _"async number: 42"_ message is printed to the screen: + +TODO image