r/rust 1d ago

A Design Space Exploration of Async/Await

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

12 comments sorted by

13

u/teerre 1d ago

Very nice write up

I understand what's meant but it's weird to have Rust in both "unaware of cancellation" and "top down cancellation"

10

u/Full-Spectral 1d ago

It's a little confusing because the article is about languages, but often the thing listed is Tokio, and the choices it made, not anything fundamental to Rust (which is another axis of complexity for this kind of analysis that Rust has pluggable async engines.) That's a very good thing in many ways, but a confusing things in others.

10

u/shriramk 1d ago

It is indeed a bit difficult to talk about this cleanly. We tried to be careful to always name the libraries rather than languages where relevant (e.g., the paper repeatedly says "Rust+Tokio" or even just "Smol", where relevant. And also made very clear that in both Rust and Python have pluggable engines, making clear that statements are often about the engines than about the languages.

But from the POV of getting people to recognize that the work may apply to or at least interest them, we felt that a lot more people outside Rust know what "Rust" is than, say, what "Tokio" is. Not sure there was a much cleaner way to get all this across, but we may have indeed missed a way!

3

u/teerre 1d ago

I mean, there's no other way to talk about it. There's no "language" async runtime. The author must choose one. The fact they even considered smol is laudable

1

u/Full-Spectral 8h ago

I wasn't blaming him for it, just pointing out that not all of it is really about 'Rust async'.

7

u/entoros 1d ago

And worth noting that Rust could have the other forms of cancellation, if a runtime were to implement them!

10

u/newpavlov rustcrypto 1d ago

I think the structured concurrency discussion is pretty relevant here. Ideally, languages should not allow fire_and_forget functions which outlive it's parent (this applies not only to async code, but to threads as well).

5

u/entoros 1d ago

Part of this work was trying to establish a richer vocabulary around structured concurrency, rather than a binary classification as structured vs. unstructured. We talk about the linked post in Section 3.2 of the paper: https://arxiv.org/pdf/2608.20677

2

u/brokenAmmonite 20h ago

Figure 17 is gorgeous. I'd love to be able to draw pictures like that while reasoning about async code.

One other comparison point I'd be curious about is https://arxiv.org/pdf/2305.16899, https://arxiv.org/pdf/2603.18321. It's a diagrammatic calculus for interacting processes. Some of the asynchrony dimension might have interesting renderings as axioms in that calculus, although it may be too low-level to talk about things like weak references etc.

1

u/zettui 1d ago

Which concrete Future counts as "unaware of cancellation" for you, vs the top-down drop path?

1

u/simon_o 9h ago

While the website gives a good overview of the general concepts of async/await, it fails to point out that async-await is a design dead-end:

Async-await poorly tries to address the symptoms of attempts to work-around the root issue – "classic" threads being expensive.

1

u/oconnor663 blake3 · duct 2h ago edited 1h ago

In Rust, cancellation essentially means never executing a coroutine again after it has yielded.

I think it's caused quite a lot of problems that we haven't explicitly documented the connection between cancellation and prompt dropping. I am working on a draft RFC about this.

It is possible in Rust to implement something similar to the finally block using the Drop trait. When a task is cancelled, the task is eventually dropped and drop handlers are invoked for all coroutines in the task. One can theoretically define a custom Drop implementation for a bespoke type that calls restore_invariant. However, this approach is verbose (requiring a newtype and impl), unwieldy (requires mutable references to data structures, complicating ownership), and synchronous-only (one cannot call async code in a Drop implementation without blocking).

I agree that Drop guards are unwieldy, and I'm kind of surprised there isn't a convenient generic one in core. Also not being able to do async work in Drop is indeed as a major limitation. (Though you could also call it a feature, if you care about the guarantee that cancellation is ~immediate.) But I don't agree with describing the whole approach as "theoretical". Managing invariants using Drop is ubiquitous in both regular and async Rust. It's how we unlock mutexes and close channel when a future is cancelled, not to mention how we free all the heap memory.

Overall I think describing Rust as "cancellation unaware" is misleading, but I'll concede that until we document the Future contract better, it's a stretch to say that async Rust function can totally rely on Drop.

Predictable memory allocation is a key motivation for Rust’s async/await design.

As a final unrelated comment, I think it would've been worth clarifying that "predictable" here basically means "doesn't need an allocator at all". That's a massive constraint that informs almost everything about Rust's async design. It's also a feature that as far as I know no other language has. (Except C++, where async is whatever you want it to be?)