The answer is no one!
Yes, it’s a bit bombastic, but I dare guess that very few programmers prefer writing a 55-line state machine when you compare it to the 7 lines of normal sequential code you’d have to write to accomplish the same thing.
If we recall the goals of most userland abstractions over concurrent operations, we’ll see that this way of doing it only checks one of the three boxes that we’re aiming for:
- Efficient
- Expressive
- Easy to use and hard to misuse
Our state machine will be efficient, but that’s pretty much it.
However, you might also notice that there is a system to the craziness. This might not come as a surprise, but the code we wrote could be much simpler if we tagged the start of each function and each point we wanted to yield control back to the caller with a few keywords and had our state machine generated for us. And that’s the basic idea behind async/await.
Let’s go and see how this would work in our example.
async/await
The previous example could simply be written as the following using async/await keywords:
async fn async_main() {
println!(“Program starting”)
let txt = Http::get(“/1000/HelloWorld”).await;
println!(“{txt}”);
let txt2 = Http::(“500/HelloWorld2”).await;
println!(“{txt2}”);
}
That’s seven lines of code, and it looks very familiar to code you’d write in a normal subroutine/function.
It turns out that we can let the compiler write these state machines for us instead of writing them ourselves. Not only that, we could get very far just using simple macros to help us, which is exactly how the current async/await syntax was prototyped before it became a part of the language. You can see an example of that at https://github.com/alexcrichton/futures-await.
The downside is of course that these functions look like normal subroutines but are in fact very different in nature. With a strongly typed language such as Rust, which borrow semantics instead of using a garbage collector, it’s impossible to hide the fact that these functions are different. This can cause a bit of confusion for programmers, who expect everything to behave the same way.
Coroutine bonus example
To show how close our example is to the behavior we get using the std::future:::Future trait and async/await in Rust, I created the exact same example as we just did in a-coroutines using “proper” futures and the async/await syntax instead. The first thing you’ll notice is that it only required very minor changes to the code. Secondly, you can see for yourself that the output shows the exact same program flow as it did in the example where we hand-wrote the state machine ourselves. You will find this example in the ch07/a-coroutines-bonus folder in the repository.
So, let’s take this a step further. To avoid confusion, and since our coroutines only yield to the calling function right now (there is no scheduler, event loop, or anything like that yet), we use a slightly different syntax called coroutine/wait and create a way to have these state machines generated for us.