Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upTracking issue for async/await (RFC 2394) #50547
Comments
withoutboats
added
B-RFC-approved
T-lang
C-tracking-issue
A-generators
E-needs-mentor
labels
May 8, 2018
withoutboats
referenced this issue
May 8, 2018
Merged
async/await notation for ergonomic asynchronous IO #2394
This comment has been minimized.
This comment has been minimized.
|
The discussion here seems to have died down, so linking it here as part of the |
This comment has been minimized.
This comment has been minimized.
|
Implementation is blocked on #50307. |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
May 10, 2018
•
|
About syntax: I'd really like to have
I agree here, but braces are evil. I think it's easier to remember that let foo = await future?It's easier to read, it's easier to refactor. I do believe it's the better approach. let foo = await!(future)?Allows to better understand an order in which operations are executed, but imo it's less readable. I do believe that once you get that If any disagreement exist, please express them so we can discuss. I don't understanda what's silent downvote stands for. We all wish good to the Rust. |
This comment has been minimized.
This comment has been minimized.
|
I have mixed views on |
This comment has been minimized.
This comment has been minimized.
It might be possible to learn that and internalise it, but there's a strong intuition that things that are touching are more tightly bound than things that are separated by whitespace, so I think it would always read wrong on first glance in practice. It also doesn't help in all cases, e.g. a function that returns a let foo = await (foo()?)?; |
This comment has been minimized.
This comment has been minimized.
|
The concern here is not simply "can you understand the precedence of a single await+ A summary of the options for
@alexreg It does. Kotlin works this way, for example. This is the "implicit await" option. |
This comment has been minimized.
This comment has been minimized.
|
@rpjohnst Interesting. Well, I'm generally for leaving |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
May 10, 2018
•
|
@alexreg async/await is really nice feature, as I work with it on day-to-day basis in C# (which is my primary language). @rpjohnst classified all possibilities very well. I prefer the second option, I agree on others considerations (noisy/unusual/...). I have been working with async/await code for last 5 years or something, it's really important to have such a flag keywords.
In my practice you never write two let first = await first()?;
let second = await first.second()?;
let third = await second.third()?;So I think it's ok if language discourages to write code in such manner in order to make the primary case simpler and better.
|
This comment has been minimized.
This comment has been minimized.
But is this because it's a bad idea regardless of the syntax, or just because the existing The postfix and implicit versions are far less ugly: first().await?.second().await?.third().await?first()?.second()?.third()? |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
May 10, 2018
•
I think it's a bad idea regardless of the syntax because having one line per For example let's take a look on real code (I have taken one piece from my project): [Fact]
public async Task Should_UpdateTrackableStatus()
{
var web3 = TestHelper.GetWeb3();
var factory = await SeasonFactory.DeployAsync(web3);
var season = await factory.CreateSeasonAsync(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
var request = await season.GetOrCreateRequestAsync("123");
var trackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, Request.TrackableStatuses.First(), "Trackable status");
var nonTrackableStatus = new StatusUpdate(DateTimeOffset.UtcNow, 0, "Nontrackable status");
await request.UpdateStatusAsync(trackableStatus);
await request.UpdateStatusAsync(nonTrackableStatus);
var statuses = await request.GetStatusesAsync();
Assert.Single(statuses);
Assert.Equal(trackableStatus, statuses.Single());
}It shows that in practice it doesn't worth to chain
Possibility to distinguish task start and task await is really important. For example, I often write code like that (again, a snippet from the project): public async Task<StatusUpdate[]> GetStatusesAsync()
{
int statusUpdatesCount = await Contract.GetFunction("getStatusUpdatesCount").CallAsync<int>();
var getStatusUpdate = Contract.GetFunction("getStatusUpdate");
var tasks = Enumerable.Range(0, statusUpdatesCount).Select(async i =>
{
var statusUpdate = await getStatusUpdate.CallDeserializingToObjectAsync<StatusUpdateStruct>(i);
return new StatusUpdate(XDateTime.UtcOffsetFromTicks(statusUpdate.UpdateDate), statusUpdate.StatusCode, statusUpdate.Note);
});
return await Task.WhenAll(tasks);
}Here we are creating N async requests and then awaiting them. We don't await on each loop iteration, but firstly we create array of async requests and then await them all at once. I don't know Kotlin, so maybe they resolve this somehow. But I don't see how you can express it if "running" and "awaiting" the task is the same. So I think that implicit version is a no-way in even much more implicit languages like C#. |
This comment has been minimized.
This comment has been minimized.
|
@Pzixel Yeah, the second option sounds like one of the more preferable ones. I've used @rpjohnst I kind of like the idea of a postfix operator, but I'm also worried about readability and assumptions people will make – it could easily get confused for a member of a |
This comment has been minimized.
This comment has been minimized.
For what it's worth, the implicit version does do this. It was discussed to death both in the RFC thread and in the internals thread, so I won't go into a lot of detail here, but the basic idea is only that it moves the explicitness from the task await to task construction- it doesn't introduce any new implicitness. Your example would look something like this: pub async fn get_statuses() -> Vec<StatusUpdate> {
// get_status_updates is also an `async fn`, but calling it works just like any other call:
let count = get_status_updates();
let mut tasks = vec![];
for i in 0..count {
// Here is where task *construction* becomes explicit, as an async block:
task.push(async {
// Again, simply *calling* get_status_update looks just like a sync call:
let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
});
}
// And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
join_all(&tasks[..])
}This is what I meant by "for this to work, the act of constructing a future must also be made explicit." It's very similar to working with threads in sync code- calling a function always waits for it to complete before resuming the caller, and there are separate tools for introducing concurrency. For example, closures and |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
May 10, 2018
•
I believe it does. I can't see here what flow would be in this function, where is points where execution breaks until await is completed. I only see Another point: Rust tend to be a language where you can express everything, close to bare metal and so on. I'd like to provide some quite artificial code, but I think it illustrates the idea: var a = await fooAsync(); // awaiting first task
var b = barAsync(); //running second task
var c = await bazAsync(); // awaiting third task
if (c.IsSomeCondition && !b.Status = TaskStatus.RanToCompletion) // if some condition is true and b is still running
{
var firstFinishedTask = await Task.Any(b, Task.Delay(5000)); // waiting for 5 more seconds;
if (firstFinishedTask != b) // our task is timeouted
throw new Exception(); // doing something
// more logic here
}
else
{
// more logic here
}Rust always tends to provide full control over what's happening.
|
This comment has been minimized.
This comment has been minimized.
Yes, this is what I meant by "this makes suspension points harder to see." If you read the linked internals thread, I made an argument for why this isn't that big of a problem. You don't have to write any new code, you just put the annotations in a different place ( I will also note that your understanding is probably wrong regardless of the syntax. Rust's
It does interrupt execution in both places. The key is that The only difference with your example is when exactly the futures start running- in yours, it's during the loop; in mine, it's during |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
May 10, 2018
•
Yes, C# tasks are executed synchronously until first suspension point. Thank you for pointing that out. var a = await fooAsync(); // awaiting first task
var b = Task.Run(() => barAsync()); //running background task somehow
// the rest of the method is the sameI've got your idea about pub async fn get_statuses() -> Vec<StatusUpdate> {
// get_status_updates is also an `async fn`, but calling it works just like any other call:
let count = get_status_updates();
let mut tasks = vec![];
for i in 0..count {
// Here is where task *construction* becomes explicit, as an async block:
task.push(async {
// Again, simply *calling* get_status_update looks just like a sync call:
let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
});
}
// And finally, launching the explicitly-constructed futures is also explicit, while awaiting the result is implicit:
join_all(&tasks[..])
}We are awaiting for all tasks at once while here pub async fn get_statuses() -> Vec<StatusUpdate> {
// get_status_updates is also an `async fn`, but calling it works just like any other call:
let count = get_status_updates();
let mut tasks = vec![];
for i in 0..count {
// Isn't "just a construction" anymore
task.push({
let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))
});
}
tasks
}We are executing them one be one. Thus I can't say what's exact behavior of this part: let status_update: StatusUpdateStruct = get_status_update(i).deserialize();
StatusUpdate::new(utc_from_ticks(status_update.update_date), status_update.status_code, status_update.note))Without having more context. And things get more weird with nested blocks. Not to mention questions about tooling etc. |
This comment has been minimized.
This comment has been minimized.
This is already true with normal sync code and closures. For example: // Construct a closure, delaying `do_something_synchronous()`:
task.push(|| {
let data = do_something_synchronous();
StatusUpdate { data }
});vs // Execute a block, immediately running `do_something_synchronous()`:
task.push({
let data = do_something_synchronous();
StatusUpdate { data }
});One other thing that you should note from the full implicit await proposal is that you can't call |
This comment has been minimized.
This comment has been minimized.
|
Regarding await syntax: What about a macro with method syntax? I can't find an actual RFC for allowing this, but I've found a few discussions (1, 2) on reddit so the idea is not unprecedented. This would allow // Postfix await-as-a-keyword. Looks as if we were accessing a Result<_, _> field,
// unless await is syntax-highlighted
first().await?.second().await?.third().await?
// Macro with method syntax. A few more symbols, but clearly a macro invocation that
// can affect control flow
first().await!()?.second().await!()?.third().await!()? |
This comment has been minimized.
This comment has been minimized.
fdietze
commented
May 11, 2018
|
There is a library from the Scala-world which simplifies monad compositions: http://monadless.io Maybe some ideas are interesting for Rust. quote from the docs:
|
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
May 11, 2018
•
I see several differences here:
Hmm, I didn't notice that. It doesn't sound good, because in my practice you often want to run async from non-async context. In C# Sometimes you may want to to block on Another consideration in favor of current |
This comment has been minimized.
This comment has been minimized.
You should also realize that the main RFC already has
This is not an issue. You can still use You just can't call
Yes, this is is a legitimate difference between the proposals. It was also covered in the internals thread. Arguably, having different interfaces for the two is useful because it shows you that the (You should also note that in Rust there is still quite a leap between
This is an advantage only for the literal |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
May 11, 2018
•
I'd say having different interfaces for the two has some advantages, because having API depended on implementation detail doesn't sound good to me. For example, you are writing a contract that is simply delegating a call to internal future fn foo(&self) -> Future<T> {
self.myService.foo()
}And then you just want to add some logging async fn foo(&self) -> T {
let result = await self.myService.foo();
self.logger.log("foo executed with result {}.", result);
result
}And it becomes a breaking change. Whoa?
It's an advantage for any
I know it and it disquiets me a lot. |
This comment has been minimized.
This comment has been minimized.
You can get around that by returning an fn foo(&self) -> impl Future<Output = T> { // Note: you never could return `Future<T>`...
async { self.my_service.foo() } // ...and under the proposal you couldn't call `foo` outside of `async` either.
}And with logging: fn foo(&self) -> impl Future<Output = T> {
async {
let result = self.my_service.foo();
self.logger.log("foo executed with result {}.", result);
result
}
}The bigger issue with having this distinction arises during the transition of the ecosystem from manual future implementations and combinators (the only way today) to async/await. But even then the proposal allows you to keep the old interface around and provide a new async one alongside it. C# is full of that pattern, for example. |
This comment has been minimized.
This comment has been minimized.
Pzixel
commented
May 12, 2018
•
|
Well, that sounds reasonable. However, I do believe such implicitness (we don't see if This code looks perfectly fine except I can't see if some request if async or sync. I believe that it's important information. For example: fn foo(&self) -> impl Future<Output = T> {
async {
let result = self.my_service.foo();
self.logger.log("foo executed with result {}.", result);
let bars: Vec<Bar> = Vec::new();
for i in 0..100 {
bars.push(self.my_other_service.bar(i, result));
}
result
}
}It's crucial to know if As you can see, I easily spotted that we have a looping await here and I asked to change it. When change was committed we got 3x page load speedup. Without |
This comment has been minimized.
This comment has been minimized.
|
Granting that every option has a downside, and that one of them should nonetheless end up being chosen... one thing that bothers me about |
This comment has been minimized.
This comment has been minimized.
@glaebhoerl You make good points; however, does syntax highlighting have no/insufficient impact on what it looks like and the way your brain processes things? At least for me color and boldness matters a great deal when reading code so I wouldn't skip over |
This comment has been minimized.
This comment has been minimized.
I strongly agree with this. I also don't see how There's also the familiarity issue @withoutboats mentioned. We can choose weird and wonderful syntax if it has some wonderful benefits. Does a postfix |
This comment has been minimized.
This comment has been minimized.
(Good question, I'm sure it would have some impact, but it's hard to guess how much without actually trying it (and substituting a different keyword only gets so far). While we're on the subject... a long time ago I mentioned that I think syntax highlighting should highlight all operators with control flow effects ( |
This comment has been minimized.
This comment has been minimized.
We agree. The notations
The syntax The
I don't think you actually need to change libsyntax at all for
As aforementioned, I argue it does due to method chaining and Rust's preference for method calls. |
This comment has been minimized.
This comment has been minimized.
This is fun. This effectively makes #[derive(Default)]
struct S {
r#await: u8
}
fn main() {
let s = ;
let z = S::default().await; // Hmmm...
} |
This comment has been minimized.
This comment has been minimized.
The idea came up a couple of times on this thread (the "implicit await" proposal).
There is
It also has a (more noticeable) preference for prefix control flow operators.
I prefer I admit might have a left-to-right bias. Note |
This comment has been minimized.
This comment has been minimized.
novacrazy
commented
Jan 15, 2019
•
|
My opinion on this seems to change every time I look at the issue, as if playing Devil’s advocate to myself. Part of that is because I’m so used to writing my own futures and state machines. A custom future with Perhaps this should be thought of another way. To me, zero-cost abstractions in Rust refers to two things: zero-cost at runtime, and more importantly zero-cost mentally. I can very easily reason about most abstractions in Rust, including futures, because they are just state machines. To this end, a simple solution should exist that introduces as little magic to the user. Sigils especially are a bad idea, as they feel unnecessarily magical. This includes Perhaps the best solution is the easiest one, the original |
This comment has been minimized.
This comment has been minimized.
I don't see how...? |
This comment has been minimized.
This comment has been minimized.
I think the operative word here is deprecated. Using
The
Those prefix control flow operators are typed at
The
This is what I mean by "APIs are structured". I think methods and method chaining are common and idiomatic in Rust. The prefix and block syntaxes compose poorly with those and with |
This comment has been minimized.
This comment has been minimized.
|
I may be in the minority with this opinion, and if so, ignore me: Would it be fair to move the prefix-vs-postfix discussion to an Internals thread, and then simply come back here with the outcome? That way we can keep the tracking issue to tracking the status of the feature? |
This comment has been minimized.
This comment has been minimized.
|
@seanmonstar Yeah, I strongly sympathize with the desire to limit discussion on tracking issues and have issues that are really just status updates. This is one of the issues I hope we can tackle with some revisions to how we manage the RFC process and issues in general. For now, I've opened a new issue here for us to use for discussion. IMPORTANT TO ALL: further |
rust-lang
locked as off topic and limited conversation to collaborators
Jan 15, 2019
This comment has been minimized.
This comment has been minimized.
|
Temporarily locking for a day to ensure that future discussion about |
rust-lang
unlocked this conversation
Jan 17, 2019
This comment has been minimized.
This comment has been minimized.
|
On Tue, Jan 15, 2019 at 07:10:32AM -0800, Pauan wrote:
Sidenote: it's always possible to use parens to make the precedence clearer:
```rust
let x = (x.do_something() await).do_another_thing() await;
let x = x.foo(|| ...).bar(|| ... ).baz() await;
```
That defeats the primary benefit of postfix await: "just keep
writing/reading". Postfix await, like postfix `?`, allows control flow
to keep moving left to right:
```rust
foo().await!()?.bar().await!()
```
If `await!` were prefix, or back when `try!` was prefix, or if you have
to parenthesize, then you have to jump back to the left hand side of
the expression when writing or reading it.
EDIT: I was reading comments beginning-to-end via email, and didn't see the "move conversation to the other issue" comments until after sending this mail.
|
This comment has been minimized.
This comment has been minimized.
ErichDonGubler
commented
Feb 25, 2019
|
Link updates: Looks like #53259 and #53447 have been closed. rust-lang-nursery/futures-rs#1199 appears to have been moved to #53548. |
This comment has been minimized.
This comment has been minimized.
|
Async-await status report: http://smallcultfollowing.com/babysteps/blog/2019/03/01/async-await-status-report/ I wanted to post a quick update on the status of the async-await Announcing the implementation working groupAs part of this push, I'm happy to announce we've formed a If you are interested in taking part, we have an "office hours" ... |
nikomatsakis
added
the
AsyncAwait-Deferred
label
Mar 5, 2019
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
ry
commented
Mar 12, 2019
|
When will |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Ekleog
commented
Mar 16, 2019
|
Another compiler issue for async/await: #59245 Also note that rust-lang-nursery/futures-rs#1199 in the top post can be checked off, as it's now fixed. |
This comment has been minimized.
This comment has been minimized.
|
It looks like there's an issue with HRLB and async closures: #59337. (Though, re-skimming the RFC it doesn't actually specify that async closures are subject to the same argument lifetime capture that async function have). |
This comment has been minimized.
This comment has been minimized.
|
Yeah, async closures have a bunch of issues and shouldn't be included in the initial round of stabilization. The current behavior can be emulated with a closure + async block, and in the future I'd love to see a version that allowed referencing the closure's upvars from the returned future. |

withoutboats commentedMay 8, 2018
•
edited by lqd
This is the tracking issue for RFC 2394 (rust-lang/rfcs#2394), which adds async and await syntax to the language.
I will be spearheading the implementation work of this RFC, but would appreciate mentorship as I have relatively little experience working in rustc.
TODO:
Unresolved questions:
await.Tryimplementations to which we want to commit