Join GitHub today
GitHub is home to over 28 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.
I'm pretty sure the current futures are intended to run in memory-constrained environments. What exactly is taking up so much space? Edit: this program takes 295KB disk space when compiled --release on my macbook (basic hello world takes 273KB): use futures::{executor::LocalPool, future};
fn main() {
let mut pool = LocalPool::new();
let hello = pool.run_until(future::ready("Hello, world!"));
println!("{}", hello);
} |
This comment has been minimized.
This comment has been minimized.
Also what do you mean by memory? I have run current async/await based code on devices with 128 kB flash/16 kB ram. There are definitely memory use issues with async/await currently, but those are mostly implementation issues and can be improved by adding some additional optimisations (e.g. #52924). |
This comment has been minimized.
This comment has been minimized.
Why? This still doesn't seem like anything that futures forces you into. You can just as easily call |
This comment has been minimized.
This comment has been minimized.
aep
commented
Nov 21, 2018
•
I don't think this is relevant. This whole discussion has detailed into invalidating a point I didnt even intent to make. I'm not here to criticize futures beyond saying that bolting its design into the core language is a mistake. My point is the async keyword can be made future proof if its done properly. Continuations is what I want, but maybe other people come up with even better ideas.
Yes that would make sense if Future:poll had call args. It cannot have them becuse poll needs to be abstract. Instead I'm proposing to emit a continuation from the async keyword, and impl Future for any Continuation with zero arguments. It's a simple, low effort change that adds no cost to futures but allows reusability of keywords that are currently exclusively for one library. But continations can of course be implemented with a preprocessor as well, which is what we're going to do. Unfortunately the desugar can only be a closure, which is more expensive than a proper continuation. |
This comment has been minimized.
This comment has been minimized.
|
@aep How would we make it possible to reuse the keywords ( |
This comment has been minimized.
This comment has been minimized.
aep
commented
Nov 23, 2018
•
|
@Centril my naive quick fix would be to lower an async to a Generator not to a Future. That'll allow time to make generator useful for proper continuations rather than being an exclusive backend for futures. Its like a 10 lines PR maybe. But i dont have the patience to fight a bee hive over it, so i'll just build a preproc to desugar a different keyword. |
This comment has been minimized.
This comment has been minimized.
|
I haven't been following the async stuff so apologies if this has been discussed before / elsewhere but what's the (implementation) plan for supporting async / await in AFAICT the current implementation uses TLS to pass a Waker around but there's no TLS (or thread) support in Is the plan to block stabilization of async / await on |
This comment has been minimized.
This comment has been minimized.
|
@japaric https://doc.rust-lang.org/nightly/std/future/trait.Future.html#tymethod.poll Edit: not relevant for the async/await, only for futures. |
This comment has been minimized.
This comment has been minimized.
I believe so. The relevant pieces are the functions in EDIT: Even without generator arguments I believe this could also be done with some slightly more complicated code in the async transform. I would personally like to see generator arguments added for other use-cases, but I pretty certain that removing the TLS requirement with/without them will be possible. |
This comment has been minimized.
This comment has been minimized.
aep
commented
Nov 30, 2018
|
@japaric Same boat. Even if someone made futures work on embedded, its very risky since its all Tier3. I figured out an ugly hack that requires far less work than fixing async: weave in an Arc through a stack of Generators.
Ideally they'd just lower async to a "pure" closure stack rather than a Future so you would not need any runtime assumptions and you could then push in the impure environment as an argument at the root. I was halfway at implemening that https://twitter.com/arvidep/status/1067383652206690307 but kind of pointless to go all the way if i'm the only one wanting it. |
This comment has been minimized.
This comment has been minimized.
|
And I couldn't stop thinking about whether TLS-less async/await without generator arguments is possible, so I implemented a It definitely requires a lot more subtle safety guarantees than the current TLS based solution or a generator argument based solution (at least when you just assume the underlying generator arguments are sound), but I'm pretty sure it's sound (as long as no one uses the rather large hygiene hole I couldn't find a way around, this wouldn't be an issue for an in-compiler implementation as it can use unnameable gensym idents to communicate between the async transform and await macro). |
This comment has been minimized.
This comment has been minimized.
|
I just realized that there's no mention of moving |
This comment has been minimized.
This comment has been minimized.
|
@Nemo157 As |
This comment has been minimized.
This comment has been minimized.
|
@Centril I don't know who told you |
This comment has been minimized.
This comment has been minimized.
|
@cramertj He meant the macro version not the keyword version i believe... |
This comment has been minimized.
This comment has been minimized.
|
@crlf0710 what's about the implicit await/explicit async-block version? |
This comment has been minimized.
This comment has been minimized.
|
@crlf0710 I did as well :) |
This comment has been minimized.
This comment has been minimized.
|
@cramertj Don't we want to remove the macro because there's currently an ugly hack in the compiler that makes the existence of both |
This comment has been minimized.
This comment has been minimized.
|
@stjepang I really don't care too much in any direction about the syntax of
I think that the best route to shipping is to land |
This comment has been minimized.
This comment has been minimized.
novacrazy
commented
Dec 18, 2018
|
I follow this issue very loosely, but here is my opinion: Personally I like the It's not any kind of magic or new syntax, just a regular macro. Less is more, after all. Then again, I also preferred Meanwhile |
This comment has been minimized.
This comment has been minimized.
|
Keep in mind that eventually we'll want async and generators to be distinct features, potentially even composable with each other (producing a |
This comment has been minimized.
This comment has been minimized.
It can't permanently be user-visible that it lowers to |
This comment has been minimized.
This comment has been minimized.
novacrazy
commented
Dec 19, 2018
Are async and generators not distinct features? Related, of course, but comparing this again to how JavaScript did it, I always thought async would be built on top of generators; that the only difference being an async function would return and yield In fact, I once wrote a library about this exact thing, recursively evaluating both async functions and generator functions that returned Promises/Futures. |
This comment has been minimized.
This comment has been minimized.
|
@cramertj It can't be implemented that way if the two are distinct "effects." There's some discussion around this here: https://internals.rust-lang.org/t/pre-rfc-await-generators-directly/7202. We don't want to |
This comment has been minimized.
This comment has been minimized.
Yes, of course that's what it'd appear like to the user, but in terms of the desugaring you just have to wrap |
This comment has been minimized.
This comment has been minimized.
|
@cramertj Also this one:
@novacrazy Yes, they are distinct features, but they should be composable together. And indeed in JavaScript they are composable: https://thenewstack.io/whats-coming-up-in-javascript-2018-async-generators-better-regex/
|
This comment has been minimized.
This comment has been minimized.
|
@Centril ok, opened #56974, is that correct enough to be added as an unresolved question to the OP? I really don't want to get into the
Note that I also said I don't believe that the macro can stay a library implemented macro (ignoring whether or not it will continue to appear as a macro to users), to expand on the reasons:
|
This comment has been minimized.
This comment has been minimized.
|
I think there's a bit of confusion here-- it was never the intention that The only important bit to understand here is that there isn't a significant semantic difference between any of the proposed syntaxes-- they're just surface syntax. |

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.