-
Notifications
You must be signed in to change notification settings - Fork 251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Write the first half of the advanced async/await chapter #236
Conversation
cc @rust-lang/wg-async |
I notice that the recent PRs have been merged with the CI red. The first breaking one seems to be 58eeade. What's the plan here? |
Signed-off-by: Nick Cameron <[email protected]>
|
||
Annoyingly, this often confuses the compiler since (unlike functions) the 'return' type of an async block is not explicitly stated. You'll probably need to add some type annotations on variables or use turbofish types to make this work, e.g., `Ok::<_, MyError>(())` instead of `Ok(())` in the above example. | ||
|
||
A function which returns an async block is pretty similar to an async function. Writing `async fn foo() -> ... { ... }` is roughly equivalent to `fn foo() -> ... { async { ... } }`. In fact, from the caller's perspective they are equivalent, and changing from one form to the other is not a breaking change. Furthermore, you can override one with the other when implementing an async trait (see below). However, you do have to adjust the type, making the `Future` explicit in the async block version: `async fn foo() -> Foo` becomes `fn foo() -> impl Future<Output = Foo>` (you might also need to make other bounds explicit, e.g., `Send` and `'static`). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Rust 2024, due to the changes to lifetime capture rules, these should be exactly equivalent. And in either case, auto traits like Send
are going to leak to the returned opaque future.
It'd probably be more accurate to say that there are times when you want or need to desugar to the -> impl Future<..>
form so that you can specify bounds on that returned opaque type rather than saying that one needs to adjust those things when converting between the forms.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tracked for later...
This looks great overall. |
Co-authored-by: Travis Cross <[email protected]>
src/part-guide/adv-async-await.md
Outdated
|
||
The first two are specific to Tokio, though most runtimes provide similar facilities. The second requires cooperation of the future being canceled, but the others do not. In these other cases, the canceled future will get no notification of cancellation and no opportunity to clean up (besides its destructor). Note that even if a future has a cancellation token, it can still be canceled via the other methods which won't trigger the cancellation token. | ||
|
||
From the perspective of writing async code (in async functions, blocks, futures, etc.), the code might stop executing at any `await` (including hidden ones in macros) and never start again. In order for your code to be correct (specifically to be *cancellation safe*), it must work correctly whether it completes normally or whether it terminates at any await point. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're introducing a new term here to users: "cancellation safety". Covering this term in the main only makes sense if we're also planning to steer people towards using select!
, which I don't think we should do. At least not in the main guide, possibly in a reference section or appendix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually: even if we weren't going to cover select!
we shouldn't cover "cancellation safety" as a good thing. By ignoring cancellation it violates the black-box model of structured concurrency.
Futures were designed with a means of cancellation directly built-in. When an API is "cancellation-safe" it means it will ignore the the standard cancellation signal (drop) by design. That means that cancellation won't just automatically propagate when a parent future is dropped, usually leading to dangling futures.
To cancel a "cancel-safe" future you need to have specific knowledge about that future. Which is easy to forget about and usually hard to use correctly. If such an API is provided at all in the first place.
I believe we should be treating "cancellation safety" as an anti-feature to be avoided. It's probably worth discussing in an appendix or separate section, but I don't think it should exist on the happy path where we're still building up a mental model for users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cancellation safety is analogous to and is as valid as unwind safety. But first, let's check we mean the same thing by it. When you say:
we shouldn't cover "cancellation safety" as a good thing
I think to myself, "well of course it's a good thing." All it means is that nothing bad (i.e. unexpected, incorrect, etc.) happens if you cancel the future. Certainly bad things happening when you cancel the future can't be a good thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a pedagogical note, whatever we think of cancellation safety, as long as it's something that we all talk about, it's something to which we probably need to introduce people.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cancellation safety is analogous to and is as valid as unwind safety.
While I agree that both properties share similarities, I'd be quick to note that unwind safety is broadly considered to be confusing, sees little usage outside libstd, and there have been repeated talk about deprecating it for those exact reasons.
Also I should probably point out that unwind safety has actually gone through the RFC process and is representable in the type system. While "cancellation safety" has not seen any RFCs and as-defined by Tokio is also unrepresentable in the type system.
I think to myself, "well of course it's a good thing." All it means is that nothing bad happens if you cancel the future.
What you're (perhaps unintentionally) arguing for here is that it's actually good for APIs to violate the standard mechanism for cancellation that was designed into the Future
API.
That can be fine, but the question to then ask should be: what do we gain from breaking from the core Future
design? From my perspective (and this has been echoed by Tokio contributors I've discussed this with) the only practical reason anyone cares about "cancellation safety" is if they want to use select!
.
Back in 2018 select!
probably seemed like a good idea. But six years and a lot of usage experience later, it seems pretty clear select!
creates more problems than it solves. It's easy to misuse, does not have its invariants checked by the compiler, depends on a different futures model, has multiple subtly different implementations, and so on.
Cancellation is one of the most cited problems people have using async Rust. I believe we should take this opportunity to take the past six years of learnings and guide a new cohort of Rust developers to instead use structured APIs for concurrency and automatic propagation of cancellation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, cc @chriskrycho who had particular thoughts about how cancellation should be introduced here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's much language work to be done, and I'm not sure to what degree we can reasonably point people toward patterns such as structured concurrency ahead of doing that work.
The only limitation I'm aware of is that if we work with multi-threaded futures, while we can automatically propagate cancellation across thread boundaries. But we have no way to wait for that cancellation to finish without risking dead-locking on single-threaded runtimes. We need async destructors for this.
That's really all. I don't think that should affect our decision to tell people that e.g. future invocations should always have a parent future that .await
s it. Or that people should use Stream::merge
and probably avoid select!
.
It seems that you're essentially asking the project to endorse, at least in this book, these libraries as the currently-best solution to these problems and for how to build programs in async Rust.
I mean: a little bit, yeah. I don't think we need to cover futures-time
here - but the APIs provided by futures-concurrency
and parallel-future
seem important enough. I assume that's also why futures-concurrency
has been included in the outline of this chapter.
@chriskrycho's ask to thoroughly cover cancellation in this book is part of why I'm taking this position. Cancellation is incredibly important and something users struggle with. And I don't know how we can setup people for success without centering structured concurrency and explicitly steering people away from APIs that break the async cancellation model.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, at this point, I'm curious to hear what the rest of @rust-lang/wg-async thinks.
Here are some of my thoughts.
This book should say something about cancellation and cancellation safety. These terms are out there and if we didn't address them, it'd be surprising to readers. For example, someone might say "I see people talking all time on Reddit about cancellation safety. Why doesn't this book address it?"
The book has a lot of freedom to define these terms. It makes sense to be clear about what is meant and how that differs from Tokio's concepts of the same name (if they differ at all).
Similarly, for better or worse, select!
is a big part of how most people experience async Rust. We should probably talk about it. I could see two approaches, mostly depending on how iconoclastic we want to be:
- Write the book mainly using
select!
, with forward references saying something like "Structuring your code robustly usingselect!
can be hard to get right. Check out Chapter NN on Structured Concurrency for another approach." Then, have a chapter that shows how structured concurrency really shines and why users should adopt that. - Write the book mainly using structured concurrency, noting that most of the ecosystem at the time of writing uses
select!
but we encourage the use of structured concurrency primitives because they are easier to get correct. Then add a chapter forselect!
, since if you're working with already-existing code, chances are you're going to need to understandselect!
.
I'd probably lean towards option 1 since I think that better reflects the state of the world today. Maybe for the second edition it'd be possible to structure the book around option 2.
I'm starting to think that cancellation safety might better be thought of as a design pattern or an API philosophy or discipline, rather than as a foundational safety property the way we think of memory safety or type safety. At least as I've been thinking about cancellation lately, it tends to be about programs such as:
let mut thing = Thagomizer::new();
thing.thagomize().timeout(ONE_SECOND).await;
thing.thagomize().await;
Basically, we're concerned about the behavior if the timeout expires. What happens on the second call to thagomize
? I don't know that we can make a blanket answer for all programs. The type system certainly cannot capture all of the behaviors and semantics of whatever you might put in place of Thagomizer
. So at least for the foreseeable future, I think there's always going to be a place for documenting those properties we can't capture in code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the book should discuss the concept of writing code that is correct with respect to cancellation (execution ending at an await point), because it can be tricky to do right, and there have been a lot of bugs caused by code not handling this correctly.
I do think "cancellation safety" is a natural enough term for that, though there might be other terms that are better. It's useful to note, as others have, that other definitions exist than the one used in this book. It's valid to choose a different term for that reason, and maybe others have suggestions, but I also think when it comes to terminology we have an opportunity to "set the agenda" in this book.
The particular definition @yoshuawuyts references on tokio::select
is referring more to a property I might venture to call "restart safety", i.e. that an expression can be re-evaluated again after its original future was dropped, without undesirable side effects.1 This is a superset of the definition of cancellation safety used in this book, and as such I think we could reclaim the term and ask them to pick a more precise one.
With respect to how we cover current tools versus emerging ones (edit: Zulip topic): I think we have to stick to the facts. In particular, we should list out the various patterns that one might encounter and the various ways of solving those patterns. We should explain that some solutions have known footguns, while others are newer and therefore less widely used and might be less general than the footgunny ones.
If we don't and try to protect users by not giving them the full picture, I think the async book never becomes relevant enough to have any influence. People need a resource that prepares them for the code that exists in the world that they need to understand, one that doesn't hide from them actual solutions they might need. It's okay to stick these in an "advanced" section with all the usual caveats, but while they are widely used we should still include them.
Footnotes
-
We don't have a good way of explaining this in terms of existing language concepts because
select
is a macro that extends the language with a new control-flow construct whose properties are unique and not widely understood. That is probably closely related to why it seems to be prone to misuse. ↩
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm tracking this issue at #227 (comment). I'll try and tighten up the language here a little for now, but intend to come back to this later. In particular, I'll have a think about how I want to talk about cancelation throughout the book.
}.await? | ||
``` | ||
|
||
Annoyingly, this often confuses the compiler since (unlike functions) the 'return' type of an async block is not explicitly stated. You'll probably need to add some type annotations on variables or use turbofished types to make this work, e.g., `Ok::<_, MyError>(())` instead of `Ok(())` in the above example. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it'd be good to make this example compile and run as is, rather than saying you'll probably need to modify it. Or does this one work and we got lucky, but in general you'll probably need a turbofish?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just a snippet, whether it compiles or not depends on the surrounding function (and also the type of foo
, probably).
Signed-off-by: Nick Cameron <[email protected]>
No description provided.