Executors

Asynchronous Rust functions return futures. Futures must have poll called on them to advance their state. Futures are composed of other futures. So, the question is, what calls poll on the very most outer future?

Recall from earlier, to run asynchronous functions, they must either be passed to tokio::spawn or be the main function annotated with #[tokio::main]. This results in submitting the generated outer future to the Tokio executor. The executor is responsible for calling Future::poll on the outer future, driving the asynchronous computation to completion.

Mini Tokio

To better understand how this all fits together, let’s implement our own minimal version of Tokio! The full code can be found here.

  1. use std::collections::VecDeque;
  2. use std::future::Future;
  3. use std::pin::Pin;
  4. use std::task::{Context, Poll};
  5. use std::time::{Duration, Instant};
  6. use futures::task;
  7. fn main() {
  8. let mut mini_tokio = MiniTokio::new();
  9. mini_tokio.spawn(async {
  10. let when = Instant::now() + Duration::from_millis(10);
  11. let future = Delay { when };
  12. let out = future.await;
  13. assert_eq!(out, "done");
  14. });
  15. mini_tokio.run();
  16. }
  17. struct MiniTokio {
  18. tasks: VecDeque<Task>,
  19. }
  20. type Task = Pin<Box<dyn Future<Output = ()> + Send>>;
  21. impl MiniTokio {
  22. fn new() -> MiniTokio {
  23. MiniTokio {
  24. tasks: VecDeque::new(),
  25. }
  26. }
  27. /// Spawn a future onto the mini-tokio instance.
  28. fn spawn<F>(&mut self, future: F)
  29. where
  30. F: Future<Output = ()> + Send + 'static,
  31. {
  32. self.tasks.push_back(Box::pin(future));
  33. }
  34. fn run(&mut self) {
  35. let waker = task::noop_waker();
  36. let mut cx = Context::from_waker(&waker);
  37. while let Some(mut task) = self.tasks.pop_front() {
  38. if task.as_mut().poll(&mut cx).is_pending() {
  39. self.tasks.push_back(task);
  40. }
  41. }
  42. }
  43. }

This runs the async block. A Delay instance is created with the requested delay and is awaited on. However, our implementation so far has a major flaw. Our executor never goes to sleep. The executor continuously loops all spawned futures and polls them. Most of the time, the futures will not be ready to perform more work and will return Poll::Pending again. The process will burn CPU and generally not be very efficient.

Ideally, we want mini-tokio to only poll futures when the future is able to make progress. This happens when a resource that the task is blocked on becomes ready to perform the requested operation. If the task wants to read data from a TCP socket, then we only want to poll the task when the TCP socket has received data. In our case, the task is blocked on the given Instant being reached. Ideally, mini-tokio would only poll the task once that instant in time has passed.

To achieve this, when a resource is polled, and the resource is not ready, the resource will send a notification once it transitions into a ready state.