Final thoughts 165 The thing to make a note of here is the elapsed time. It's now just over four seconds, just like we expected it would be when our futures run concurrently. If we take a look at how coroutine/await changed the experience of writing coroutines from a programmer's perspective, we'll see that we're much closer to our goal now: Efficient : State machines require no context switches and only save/restore the data associated with that specific task. We have no growing vs segmented stack issues, as they all use the same OS-provided stack. Expressive : We can write code the same way as we do in “normal” Rust, and with compiler support, we can get the same error messages and use the same tooling Easy to use and hard to misuse : This is a point where we probably fall slightly short of a typical fiber/green threads implementation due to the fact that our programs are heavily transformed “behind our backs” by the compiler, which can result in some rough edges. Specifically, you can't call an async function from a normal function and expect anything meaningful to happen; you have to actively poll it to completion somehow, which gets more complex as we start adding runtimes into the mix. However, for the most part, we can write programs just the way we're used to. Final thoughts Before we round off this chapter, I want to point out that it should now be clear to us why coroutines aren't really pre-emptable. If you remember back in Chapter 2, we said that a stackful coroutine (such as our fibers/green threads example) could be pre-empted and its execution could be paused at any point. That's because they have a stack, and pausing a task is as simple as storing the current execution state to the stack and jumping to another task. That's not possible here. The only places we can stop and resume execution are at the pre-defined suspension points that we manually tagged with wait. In theory, if you have a tightly integrated system where you control the compiler, the coroutine definition, the scheduler, and the I/O primitives, you could add additional states to the state machine and create additional points where the task could be suspended/resumed. These suspension points could be opaque to the user and treated differently than normal wait/suspension points. For example, every time you encounter a normal function call, you could add a suspension point (a new state to our state machine) where you check in with the scheduler if the current task has used up its time budget or something like that. If it has, you could schedule another task to run and resume the task at a later point even though this didn't happen in a cooperative manner. However, even though this would be invisible to the user, it's not the same as being able to stop/resume execution from any point in your code. It would also go against the usually implied cooperative nature of coroutines.