Comment by suddenlybananas
16 hours ago
Could someone explain this to me as someone who's never touched async Rust? What kind of useful patterns would this allow for?
16 hours ago
Could someone explain this to me as someone who's never touched async Rust? What kind of useful patterns would this allow for?
The big one is scoped tasks, or structured concurrency.
Currently, Rust has scoped threads: Threads that are guaranteed to terminate before the function that spawned them returns. This is powerful because it allows you to pass references to data that lives on your own stack to threads that you spawn, without any bookkeeping or synchronization mechanism - just the normal borrow checker rules.
For example, you can allocate a large array, then split it into multiple non-overlapping slices, and then have a group of threads populate each slice, all in safe Rust code.
But the same isn't true for async tasks in Rust, because futures are just objects representing a state machine, and they don't get any special treatment. In particular, they carry no guarantee that the state machine will actually run to completion, which is fundamentally different from how functions run (stack frames are guaranteed to unwind in some way, either by returning or panicking, unless the entire program has terminated).
To make the situation worse, there are many cases where Rust futures are much more prone to cancellation than synchronous code, because that is also one of the big benefits of using async in the first place - for example, you may be running multiple futures in parallel, pick the result from the one that finishes first, and then cancel the rest.
Getting this stuff under control is why people say that "async cancellation" is a difficult problem to solve, and that is true in all languages that have async. These traits will hopefully make it much easier to work with in Rust.
(There are also many other interesting things you could do with this, unrelated to async. Immovable and unforgettable are both interesting properties of an object that could be used to design many cool APIs in general.)
Things you'd expect to work already.
It doesn't really add anything new and flashy, but removes some annoying warts.
Sync code has scoped threads that enable multi-threaded execution within a function, without having to ensure the data outlives the function call. Async can't do that while guaranteeing safety. This makes tokio::spawn awkward and annoying, and is a major source why people dislike Rust's async.
Low-level async code that polls Futures requires using the Pin wrapper type, which is unergonomic, and doesn't really guarantee safety, but it's more like a "be careful here" sign. Proposed changes would make that code look more like normal Rust and work without unsafe escape hatches.
> and is a major source why people dislike Rust's async
It's worth mentioning that there is, in fact, no language out there other than Rust that can even do this in the first place.
Some languages give the illusion that they support it by boxing the stack frame of async functions and letting a garbage collector deal with the consequences, but that comes with significant drawbacks too (additional GC pressure, heap allocation overhead, requiring a GC in the first place).
You can do it with C++ coroutines, but it's much harder to do correctly than in Rust if you want to maintain any sense of conviction that the system is correct.
The main reason that structured async concurrency would be so awesome to have is that it feels like Rust has the right set of features that could enable it with a set of constraints that are so much more attractive than any other language out there can provide - no overhead, "just works" with no drawbacks.
(For the record, you can actually get pretty far today using primitives like `FuturesUnordered` instead of `tokio::spawn` and similar, but this sidesteps the runtime's scheduler, so YMMV. This basically creates a task-local mini-scheduler for your futures, which may or may not be sufficient.)
C++26 adopted senders/receivers (std::execution) as its official concurrency model, with the explicit aim of supporting structured concurrency. See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p23...
It makes it easier to write recursive async functions. It makes it easier for async functions to borrow rather than clone from their outer scope.
All really awesome, non controversial and ergonomic things.
Wait, how does it actually change the recursive async story?
The problem today is that the compiler-synthesized struct implementing `Future` for each async function cannot contain an instance of itself without boxing, because it would create a type of infinite size. That's a separate problem that's also hard to solve nicely, because the call tree might be deep, and deciding where to cut (using Box::pin) is non-trivial.