In this step, we’ll create an executor that will:
- Hold many top-level futures and switch between them
- Enable us to spawn new top-level futures from anywhere in our asynchronous program
- Hand out Waker types so that they can sleep when there is nothing to do and wake up when one of the top-level futures can progress
- Enable us to run several executors by having each run on its dedicated OS thread
Note
It’s worth mentioning that our executor won’t be fully multithreaded in the sense that tasks/futures can’t be sent from one thread to another, and the different Executor instances will not know of each other. Therefore, executors can’t steal work from each other (no work-stealing), and we can’t rely on executors picking tasks from a global task queue.
The reason is that the Executor design will be much more complex if we go down that route, not only because of the added logic but also because we have to add constraints, such as requiring everything to be Send + Sync.
Some of the complexity in asynchronous Rust today can be attributed to the fact that many runtimes in Rust are multithreaded by default, which makes asynchronous Rust deviate more from “normal” Rust than it actually needs to.
It’s worth mentioning that since most production runtimes in Rust are multithreaded by default, most of them also have a work-stealing executor. This will be similar to the last version of our bartender example in Chapter 1, where we achieved a slightly increased efficiency by letting the bartenders “steal” tasks that are in progress from each other.
However, this example should still give you an idea of how we can leverage all the cores on a machine to run asynchronous tasks, giving us both concurrency and parallelism, even though it will have limited capabilities.
Let’s start by opening up executor.rs located in the runtime subfolder.
This file should already contain our Waker and the dependencies we need, so let’s start by adding the following lines of code just below our dependencies:
ch08/b-reactor-executor/src/runtime/executor.rs
type Task = Box<dyn Future<Output = String>>;
thread_local!
{
static CURRENT_EXEC: ExecutorCore = ExecutorCore::default();
}
The first line is a type alias; it simply lets us create an alias called Task that refers to the type: Box<dyn Future<Output = String>>. This will help keep our code a little bit cleaner.
The next line might be new to some readers. We define a thread-local static variable by using the thread_local! macro.
The thread_local! macro lets us define a static variable that’s unique to the thread it’s first called from. This means that all threads we create will have their own instance, and it’s impossible for one thread to access another thread’s CURRENT_EXEC variable.
We call the variable CURRENT_EXEC since it holds the Executor that’s currently running on this thread.
The next lines we add to this file is the definition of ExecutorCore:
ch08/b-reactor-executor/src/runtime/executor.rs
#[derive(Default)]
struct ExecutorCore {
tasks: RefCell<HashMap<usize, Task>>,
ready_queue: Arc<Mutex<Vec<usize>>>,
next_id: Cell<usize>,
}