Many programming languages now provide the async / await keywords for expressing concurrency. The design rationale is pretty consistent: to make concurrent programs look more like straight-line code (see: Python, Rust, or Swift). We therefore describe the paradigm which encompasses async/await as straight-line asynchrony, as opposed to using event loops or callbacks.
Language designs for straight-line asynchrony have been brewing for over 15 years. In this project, we wanted to understand: how similar or different is async/await between languages? The short answer is a lot more different than we expected. We wrote a paper, “A Design Space Exploration of Async/Await”, to explain how.
So you think you know async/await? To demonstrate how much modern languages can diverge, here’s a small async program written in pseudocode. One function writes to a log, and another fires off the log write as a background task and moves on. async fn write_to_log () : print ( "A" ) // simulate a slow log write await sleep ( 2 ) print ( "B" ) async fn fire_and_forget () : task = spawn write_to_log () // return without awaiting the task async fn main () : await fire_and_forget () await sleep ( 1 ) print ( "C" ) What would you expect this program to print? Check my answer There isn’t really a right answer, because you were probably right for some language. Below is how seven modern async runtimes actually behave: Show me the outputs Four different answers, for a program whose entire job is to write a log line in the background. And it gets worse. In the paper, we show that across the seven runtimes, no two produce the same output for three variations of this simple program! Do you actually know your language’s async semantics?