block_on will be the entry point to our Executor. Often, you will pass in one top-level future first, and when the top-level future progresses, it will spawn new top-level futures onto our executor. Each new future can, of course, spawn new futures onto the Executor too, and that’s how an asynchronous program basically works.
In many ways, you can view this first top-level future in the same way you view the main function in a normal Rust program. spawn is similar to thread::spawn, with the exception that the tasks stay on the same OS thread in this example. This means the tasks won’t be able to run in parallel, which in turn allows us to avoid any need for synchronization between tasks to avoid data races.
Let’s go through the function step by step:
- The first thing we do is spawn the future we received onto ourselves. There are many ways this could be implemented, but this is the easiest way to do it.
- Then, we have a loop that will run as long as our asynchronous program is running.
- Every time we loop, we create an inner while let Some(…) loop that runs as long as there are tasks in ready_queue.
- If there is a task in ready_queue, we take ownership of the Future object by removing it from the collection. We guard against false wakeups by just continuing if there is no future there anymore (meaning that we’re done with it but still get a wakeup). This will, for example, happen on Windows since we get a READABLE event when the connection closes, but even though we could filter those events out, mio doesn’t guarantee that false wakeups won’t happen, so we have to handle that possibility anyway.
- Next, we create a new Waker instance that we can pass into Future::poll(). Remember that this Waker instance now holds the id property that identifies this specific Future trait and a handle to the thread we’re currently running on.
- The next step is to call Future::poll.
- If we get NotReady in return, we insert the task back into our tasks collection. I want to emphasize that when a Future trait returns NotReady, we know it will arrange it so that Waker::wake is called at a later point in time. It’s not the executor’s responsibility to track the readiness of this future.
- If the Future trait returns Ready, we simply continue to the next item in the ready queue. Since we took ownership over the Future trait, this will drop the object before we enter the next iteration of the while let loop.
- Now that we’ve polled all the tasks in our ready queue, the first thing we do is get a task count to see how many tasks we have left.
- We also get the name of the current thread for future logging purposes (it has nothing to do with how our executor works).
- If the task count is larger than 0, we print a message to the terminal and call thread::park(). Parking the thread will yield control to the OS scheduler, and our Executor does nothing until it’s woken up again.
- If the task count is 0, we’re done with our asynchronous program and exit the main loop.
That’s pretty much all there is to it. By this point, we’ve covered all our goals for step 2, so we can continue to the last and final step and implement a Reactor for our runtime that will wake up our executor when something happens.