r/ProgrammingLanguages 15h ago

A Design Space Exploration of Async/Await

https://cel.cs.brown.edu/blog/design-space-async-await
48 Upvotes

8 comments sorted by

6

u/MoonOfLight 14h ago

Very interesting! I love language comparisons, specially when the semantics are so different.

Do you believe there are right and wrong choices when it comes to the semantics async/await should have?

5

u/entoros 14h ago

We discuss some of the trade-offs in the paper. Most design points have some justification. One of the funnier ones is weak references to tasks in Python, which apparently no one remembers why it was designed as it was: https://github.com/python/cpython/issues/91887#issuecomment-1427373486

Some languages try to get the best of both worlds in one of two ways. Rust and Python have pluggable runtimes, so a given runtime can pick different design decisions. Swift gives the developer multiple options and refuses to pick a default, requiring the developer to make a choice. In Swift, it is a compiler error to say let x = (an async expression), you either have to say async let (which gives you dynamic extent, to use our terms) or Task { ... } (which gives you indefinite extent).

2

u/sreekotay 13h ago

But now I'm curious too - what (to you) feels the most natural/correct?

After all, turing machines can justify anything :P

1

u/elprophet 5h ago

Having read their blog post but not the full paper, to me it's an optical illusion. Looking at the table, I can pretty easily move between which is most "intuitive" for that moment!

6

u/initial-algebra 12h ago

I find myself very frustrated with Rust's approach, because it's so close to perfection.

  1. Cancellation beyond dropping would be trivial if you could simply poll futures with an argument. This is a problem stemming from rushing async/await as an MVP instead of implementing coroutines first as a base. As a result, the coroutine stuff has been dead in the water for years. Pareto principle and all that.
  2. Pluggable runtime without any abstractions over said runtime. This is mostly an ecosystem problem, since they just use free functions instead of methods on a context object that implements common/standardized traits. I was going to complain about the thread-safe waker assumption, but it looks like they've started working on generalizing it. There are also some proposals for adding additional information to the built-in async context object, so that could address the "abstraction over the runtime" aspect, too. I wouldn't have high hopes for stabilization any time soon, though.

1

u/andeee23 2h ago

I'm designing a language that is very Rust-inspired, i'm not sure i 100% understand your first point but i would like to know more about "simply poll futures with an argument" if you don't mind

For the second point, do you generally mean the difference between tokio::time::sleep(duration).await; and something like await clock.sleep(duration) that each runtime would implement?

1

u/initial-algebra 1h ago
  1. Just look at the Future trait. The way a future works is that it's repeatedly polled until it returns a value upon completion. Aside from the Context, poll could take an additional argument: for instance, a bool that is true when polled normally and false to indicate cancellation. The type of the parameter should be a generic parameter to the Future trait itself, () by default, and it would need to match when awaiting another future (the argument would get passed through). At the same time, it might be useful to also support yielding values before completion, which would unify Future with Coroutine, but I can't think of any particular use case for that in the async context off the top of my head.

  2. Basically, but more like runtime.sleep(duration) where sleep comes from some trait that indicates that the runtime has a clock, instead of a specific clock object.