ExecutorCore holds all the state for our Executor:
- tasks – This is a HashMap with a usize as the key and a Task (remember the alias we created previously) as data. This will hold all the top-level futures associated with the executor on this thread and allow us to give each an id property to identify them. We can’t simply mutate a static variable, so we need internal mutability here. Since this will only be callable from one thread, a RefCell will do so since there is no need for synchronization.
- ready_queue – This is a simple Vec<usize> that stores the IDs of tasks that should be polled by the executor. If we refer back to Figure 8.7, you’ll see how this fits into the design we outlined there. As mentioned earlier, we could store something such as an Arc<dyn Future<…>> here instead, but that adds quite a bit of complexity to our example. The only downside with the current design is that instead of getting a reference to the task directly, we have to look it up in our tasks collection, which takes time. An Arc<…> (shared reference) to this collection will be given to each Waker that this executor creates. Since the Waker can (and will) be sent to a different thread and signal that a specific task is ready by adding the task’s ID to ready_queue, we need to wrap it in an Arc<Mutex<…>>.
- next_id – This is a counter that gives out the next available I, which means that it should never hand out the same ID twice for this executor instance. We’ll use this to give each top-level future a unique ID. Since the executor instance will only be accessible on the same thread it was created, a simple Cell will suffice in giving us the internal mutability we need.
ExecutorCore derives the Default trait since there is no special initial state we need here, and it keeps the code short and concise.
The next function is an important one. The spawn function allows us to register new top-level futures with our executor from anywhere in our program:
ch08/b-reactor-executor/src/runtime/executor.rs
pub fn spawn<F>(future: F)
where
F: Future<Output = String> + ‘static,
{
CURRENT_EXEC.with(|e| {
let id = e.next_id.get();
e.tasks.borrow_mut().insert(id, Box::new(future));
e.ready_queue.lock().map(|mut q| q.push(id)).unwrap();
e.next_id.set(id + 1);
});
}
The spawn function does a few things:
- It gets the next available ID.
- It assigns the ID to the future it receives and stores it in a HashMap.
- It adds the ID that represents this task to ready_queue so that it’s polled at least once (remember that Future traits in Rust don’t do anything unless they’re polled at least once).
- It increases the ID counter by one.
The unfamiliar syntax accessing CURRENT_EXEC by calling with and passing in a closure is just a consequence of how thread local statics is implemented in Rust. You’ll also notice that we must use a few special methods because we use RefCell and Cell for internal mutability for tasks and next_id, but there is really nothing inherently complex about this except being a bit unfamiliar.