-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
Tracking issue for async/await (RFC 2394) #50547
Comments
The discussion here seems to have died down, so linking it here as part of the |
Implementation is blocked on #50307. |
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. |
I have mixed views on |
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()?)?; |
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. |
@rpjohnst Interesting. Well, I'm generally for leaving |
@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.
|
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()? |
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#. |
@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 |
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 |
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.
|
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 |
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 same I'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 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 |
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!()? |
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:
|
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 |
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 |
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. |
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. |
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 |
@withoutboats to my understanding the final syntax is agreed upon already, maybe it's time to mark it as Done? 😊 |
Is the intent to stabilize in time for the next beta cut on July 4, or will blocking bugs require another cycle to resolve? There are plenty of open issues under the A-async-await tag, though I'm not sure how many of those are critical. |
Aha, disregard that, I've just discovered the AsyncAwait-Blocking label. |
Hello there! When we have to expect the stable release of this feature? And how can I use that in nightly builds? |
@MehrdadKhnzd #62149 contains information about the target release date and more |
Is there plan to automatically implement Specifically I'm wondering if Unpin is not available automatically due to generated Future code itself, or whether cuz we can use references as arguments |
@DoumanAsh I suppose if an async fn never has any active self-references at yield points then the generated Future could conceivably implement Unpin, maybe? |
I think that would need to be accompanied by some pretty helpful error messages saying "not |
The stabilization PR at #63209 notes that "All the blockers are now closed." and was landed into nightly on August 20, therefore heading for the beta cut later this week. It seems worth noting that since August 20 some new blocking issues have been filed (as tracked by the AsyncAwait-Blocking tag). Two of these (#63710, #64130) appear to be nice-to-haves that would not actually impede stabilization, however there are three other issues (#64391, #64433, #64477) which seem worth discussing. These latter three issues are related, all of them arising due to PR #64292, which itself was landed to address AsyncAwait-Blocking issue #63832. A PR, #64584, has already landed in an attempt to address the bulk of the problems, but the three issues remain open for now. The silver lining is that the three serious open blockers appear to concern code that should compile, but does not currently compile. In that sense, it would be backwards-compatible to land fixes later without impeding the beta-ization and eventual stabilization of async/await. However, I'm wondering whether anyone from the lang team thinks that anything here is concerning enough to suggest that async/await should bake on nightly for another cycle (which, as distasteful as that may sound, is the point of the rapid release schedule after all). |
@bstrie We are just reusing "AsyncAwait-Blocking" for lack of a better label to note them as "high priority", they are not actually blocking. We should revamp the labeling system soon to make it less confusing, cc @nikomatsakis. |
This comment has been minimized.
This comment has been minimized.
@earthengine I don't think that's a fair assessment of the situation. The issues that have come up have all been worth taking seriously. It wouldn't be good to land async await only for people to then run into these issues trying to use it in practice :) |
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:
async_await
in Rust 1.39.0 #63209Unresolved questions:
await
.for<'r> 'r : 'static
is not satisfied” with'static
across await point #53548 (initially “expected bound lifetime parameter, found concrete lifetime” with'static
across await point futures-rs#1199)Try
implementations to which we want to commitThe text was updated successfully, but these errors were encountered: