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

Open
withoutboats opened this Issue May 8, 2018 · 233 comments

Comments

Projects
None yet
@withoutboats
Contributor

withoutboats commented May 8, 2018

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:

  • Implement
  • Stabilize
  • Document

Unresolved questions:

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented May 8, 2018

The discussion here seems to have died down, so linking it here as part of the await syntax question: https://internals.rust-lang.org/t/explicit-future-construction-implicit-await/7344

@withoutboats

This comment has been minimized.

Contributor

withoutboats commented May 9, 2018

Implementation is blocked on #50307.

@berkus berkus referenced this issue May 10, 2018

Open

Futures v0.2 #98

@Pzixel

This comment has been minimized.

Pzixel commented May 10, 2018

About syntax: I'd really like to have await as simple keyword. For example, let's look on a concern from the blog:

We aren’t exactly certain what syntax we want for the await keyword. If something is a future of a Result - as any IO future likely to be - you want to be able to await it and then apply the ? operator to it. But the order of precedence to enable this might seem surprising - await io_future? would await first and ? second, despite ? being lexically more tightly bound than await.

I agree here, but braces are evil. I think it's easier to remember that ? has lower precedence than await and end with it:

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 await foo? executes await first then you have no problems with it. It's probably lexically more tied, but await is on the left side and ? is on the right one. So it's still logical enough to await first and handle Result after it.


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.

@alexreg

This comment has been minimized.

Contributor

alexreg commented May 10, 2018

I have mixed views on await being a keyword, @Pzixel. While it certainly has an aesthetic appeal, and is perhaps more consistent, given async is a keyword, "keyword bloat" in any language is a real concern. That said, does having async without await even make any sense, feature wise? If it does, perhaps we can leave it as is. If not, I'd lean towards making await a keyword.

@Nemo157

This comment has been minimized.

Contributor

Nemo157 commented May 10, 2018

I think it's easier to remember that ? has lower precedence than await and end with it

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 Result<impl Future, _>:

let foo = await (foo()?)?;
@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented May 10, 2018

The concern here is not simply "can you understand the precedence of a single await+?," but also "what does it look like to chain several awaits." So even if we just picked a precedence, we would still have the problem of await (await (await first()?).second()?).third()?.

A summary of the options for await syntax, some from the RFC and the rest from the RFC thread:

  • Require delimiters of some kind: await { future }? or await(future)? (this is noisy).
  • Simply pick a precedence, so that await future? or (await future)? does what is expected (both of these feel surprising).
  • Combine the two operators into something like await? future (this is unusual).
  • Make await postfix somehow, as in future await? or future.await? (this is unprecedented).
  • Use a new sigil like ? did, as in future@? (this is "line noise").
  • Use no syntax at all, making await implicit (this makes suspension points harder to see). For this to work, the act of constructing a future must also be made explicit. This is the subject of the internals thread I linked above.

That said, does having async without await even make any sense, feature wise?

@alexreg It does. Kotlin works this way, for example. This is the "implicit await" option.

@alexreg

This comment has been minimized.

Contributor

alexreg commented May 10, 2018

@rpjohnst Interesting. Well, I'm generally for leaving async and await as explicit features of the language, since I think that's more in the spirit of Rust, but then I'm no expert on asynchronous programming...

@Pzixel

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.

@rpjohnst

So even if we just picked a precedence, we would still have the problem of await (await (await first()?).second()?).third()?.

In my practice you never write two await's in one line. In very rare cases when you need it you simply rewrite it as then and don't use await at all. You can see yourself that it's much harder to read than

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.

hero away future await? looks interesting although unfamiliar, but I don't see any logical counterarguments against that.

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented May 10, 2018

In my practice you never write two await's in one line.

But is this because it's a bad idea regardless of the syntax, or just because the existing await syntax of C# makes it ugly? People made similar arguments around try!() (the precursor to ?).

The postfix and implicit versions are far less ugly:

first().await?.second().await?.third().await?
first()?.second()?.third()?
@Pzixel

This comment has been minimized.

Pzixel commented May 10, 2018

But is this because it's a bad idea regardless of the syntax, or just because the existing await syntax of C# makes it ugly?

I think it's a bad idea regardless of the syntax because having one line per async operation is already complex enough to understand and hard to debug. Having them chained in a single statement seems to be even worse.

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 awaits even if syntax allows it, because it would become completely unreadable await just makes oneliner even harder to write and read, but I do believe it's not the only reason why it's bad.

The postfix and implicit versions are far less ugly

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#.
In Rust with its rules that doesn't even allow you to implicitly convert u8 to i32 it would be much more confusing.

@alexreg

This comment has been minimized.

Contributor

alexreg commented May 10, 2018

@Pzixel Yeah, the second option sounds like one of the more preferable ones. I've used async/await in C# too, but not very much, since I haven't programmed principally in C# for some years now. As for precedence, await (future?) is more natural to me.

@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 struct named await.

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented May 10, 2018

Possibility to distinguish task start and task await is really important.

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 thread::spawn/join correspond to async blocks and join_all/select/etc.

@Pzixel

This comment has been minimized.

Pzixel commented May 10, 2018

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.

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 async block which says "hello, somewhere here there are async functions, try to find out which ones, you will be surprised!".

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. await allow you to specify points where continuation process. It also allows you to unwrap a value inside future. If you allows implicit conversion on use side, it has several implications:

  1. First of all, you have to write some dirty code to just emulate this behaviour.
  2. Now RLS and IDEs should expect that our value is either Future<T> or awaited T itself. It's not an issue with keywords - it it exists, then result is T, otherwise it's Future<T>
  3. It makes code harder to understand. In you example I don't see why it does interrupt execution at get_status_updates line, but it doesn't on get_status_update. They are quite similar to each other. So it's either doesn't work the way original code was or it's so much complicated that I can't see it even when I'm quite familiar with the subject. Both alternatives don't make this option a favor.
@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented May 10, 2018

I can't see here what flow would be in this function, where is points where execution breaks until await is completed.

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 (async blocks instead of awaited expressions). IDEs have no problem telling what the type is (it's always T for function calls and Future<Output=T> for async blocks).

I will also note that your understanding is probably wrong regardless of the syntax. Rust's async functions do not run any code at all until they are awaited in some way, so your b.Status != TaskStatus.RanToCompletion check will always pass. This was also discussed to death in the RFC thread, if you're interested in why it works this way.

In you example I don't see why it does interrupt execution at get_status_updates line, but it doesn't on get_status_update. They are quite similar to each other.

It does interrupt execution in both places. The key is that async blocks don't run until they are awaited, because this is true of all futures in Rust, as I described above. In my example, get_statuses calls (and thus awaits) get_status_updates, then in the loop it constructs (but does not await) count futures, then it calls (and thus awaits) join_all, at which point those futures concurrently call (and thus await) get_status_update.

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 join_all. But this is a fundamental part of how Rust futures work, not anything to do with the implicit syntax or even with async/await at all.

@Pzixel

This comment has been minimized.

Pzixel commented May 10, 2018

I will also note that your understanding is probably wrong regardless of the syntax. Rust's async functions do not run any code at all until they are awaited in some way, so your b.Status != TaskStatus.RanToCompletion check will always pass.

Yes, C# tasks are executed synchronously until first suspension point. Thank you for pointing that out.
However, it doesn't really matter because I still should be able to run some task in background while executing the rest of the method and then check if background task is finished. E.g. it could be

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 async blocks and as I see they are the same beast, but with more disadvantages. In original proposal each async task is paired with await. With async blocks each task would be paired with async block at construction point, so we are in almost same situation as before (1:1 relationship), but even a bit worse, because it feel more unnatural, and harder to understand, because callsite behavior becomes context-depended. With await I can see let a = foo() or let b = await foo() and I would know it this task is just constructed or constructed and awaited. If i see let a = foo() with async blocks I have to look if there is some async above, if I get you right, because in this case

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.

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented May 11, 2018

callsite behavior becomes context-depended

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 async fns from non-async contexts. This means that the function call syntax some_function(arg1, arg2, etc) always runs some_function's body to completion before the caller continues, regardless of whether some_function is async. So entry into an async context is always marked explicitly, and function call syntax is actually more consistent.

@jplatte

This comment has been minimized.

Contributor

jplatte commented May 11, 2018

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 await to work in postfix position without making it a keyword / introducing new syntax for only this feature.

// 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!()?
@fdietze

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:

Most mainstream languages have support for asynchronous programming using the async/await idiom or are implementing it (e.g. F#, C#/VB, Javascript, Python, Swift). Although useful, async/await is usually tied to a particular monad that represents asynchronous computations (Task, Future, etc.).

This library implements a solution similar to async/await but generalized to any monad type. This generalization is a major factor considering that some codebases use other monads like Task in addition to Future for asynchronous computations.

Given a monad M, the generalization uses the concept of lifting regular values to a monad (T => M[T]) and unlifting values from a monad instance (M[T] => T). > Example usage:

lift {
  val a = unlift(callServiceA())
  val b = unlift(callServiceB(a))
  val c = unlift(callServiceC(b))
  (a, c)
}

Note that lift corresponds to async and unlift to await.

@Pzixel

This comment has been minimized.

Pzixel commented May 11, 2018

This is already true with normal sync code and closures. For example:

I see several differences here:

  1. Lambda context is unavoidable, but it's not for await. With await we don't have a context, with async we have to have one. The former wins, because it provide the same features, but require knowing less about the code.
  2. Lambdas tends to be short, several lines at most so we see the entire body at once, and simple. async functions may be quite big (as big, as regular functions) and complicated.
  3. Lambdas are rarely nested (except for then calls, but it's await is proposed for), async blocks are nested frequently.

One other thing that you should note from the full implicit await proposal is that you can't call async fns from non-async contexts.

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# async is just a keyword that allows compiler to rewrite function body, it doesn't affect function interface in any way so async Task<Foo> and Task<Foo> are completely interchangeable, and it decouples implementation and API.

Sometimes you may want to to block on async task, e.g when you want to call some network API from main. You have to block (otherwise you return to the OS and the program ends) but you have to run async HTTP request. I'm not sure what solution could be here except hacking main to allow it to be async as well as we do with Result main return type, if you cannot call it from non-async main.

Another consideration in favor of current await is how it works in other popular language (as noted by @fdietze ). It makes it easier to migrate from other language such as C#/TypeScript/JS/Python and thus is a better approach in terms of drumming up new people.

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented May 11, 2018

I see several differences here

You should also realize that the main RFC already has async blocks, with the same semantics as the implicit version, then.

It doesn't sound good, because in my practice you often want to run async from non-async context.

This is not an issue. You can still use async blocks in non-async contexts (which is fine because they just evaluate to a F: Future as always), and you can still spawn or block on futures using exactly the same API as before.

You just can't call async fns, but instead wrap the call to them in an async block- as you do regardless of the context you're in, if you want a F: Future out of it.

async is just a keyword that allows compiler to rewrite function body, it doesn't affect function interface in any way

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 async fn version will not run any code as part of construction, while the -> impl Future version may e.g. initiate a request before giving you a F: Future. It also makes async fns more consistent with normal fns, in that calling something declared as -> T will always give you a T, regardless of whether it's async.

(You should also note that in Rust there is still quite a leap between async fn and the Future-returning version, as described in the RFC. The async fn version does not mention Future anywhere in its signature; and the manual version requires impl Trait, which carries with it some problems to do with lifetimes. This is, in fact, part of the motivation for async fn to begin with.)

It makes it easier to migrate from other language such as C#/TypeScript/JS/Python

This is an advantage only for the literal await future syntax, which is fairly problematic on its own in Rust. Anything else we might end up with also has a mismatch with those languages, while implicit await at least has a) similarities with Kotlin and b) similarities with synchronous, thread-based code.

@Pzixel

This comment has been minimized.

Pzixel commented May 11, 2018

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

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?

This is an advantage only for the literal await future syntax, which is fairly problematic on its own in Rust. Anything else we might end up with also has a mismatch with those languages, while implicit await at least has a) similarities with Kotlin and b) similarities with synchronous, thread-based code.

It's an advantage for any await syntax, await foo/foo await/foo@/foo.await/... once you get that it's the same thing, the only difference is that you place it before/after or have a sigil instead of keyword.

You should also note that in Rust there is still quite a leap between async fn and the Future-returning version, as described in the RFC

I know it and it disquiets me a lot.

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented May 11, 2018

And it becomes a breaking change.

You can get around that by returning an async block. Under the implicit await proposal, your example looks like this:

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.

@Pzixel

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 foo() here is async or sync function) lead to the same problems that arised in protocols such as COM+ and was a reason for WCF being implemented as it was. People had problems when async remote requests were looking like simple methods calls.

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 bar is sync or async function. I often see await in the loop as a marker that this code have to be changed to achieve better throughout load and performance. This is a code I reviewed yesterday (code is suboptimal, but it's one of review iterations):

image

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 await I could easily overlook this misbehaviour.

@tikue

This comment has been minimized.

Contributor

tikue commented Nov 21, 2018

not in mine, where execution speed is irrelevant, but i have 1MB total memory, so futures.rs doesnt even fit on flash

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);
}
@Nemo157

This comment has been minimized.

Contributor

Nemo157 commented Nov 21, 2018

not in mine, where execution speed is irrelevant, but i have 1MB total memory, so futures.rs doesnt even fit on flash

I'm pretty sure the current futures are intended to run in memory-constrained environments. What exactly is taking up so much space?

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).

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented Nov 21, 2018

A common example would be that you cant just invoke the future stack when you know a file descriptor of a socket is ready, but instead have to implement all the execution logic to map real world events to futures, which has external cost such as locking, code size and most importantly code complexity.

Why? This still doesn't seem like anything that futures forces you into. You can just as easily call poll as you would a push-based mechanism.

@aep

This comment has been minimized.

aep commented Nov 21, 2018

Also what do you mean by memory?

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.

You can just as easily call poll as you would a push-based mechanism.

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.

@Centril

This comment has been minimized.

Contributor

Centril commented Nov 22, 2018

@aep How would we make it possible to reuse the keywords (async and await)?

@aep

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.

@japaric

This comment has been minimized.

Member

japaric commented Nov 30, 2018

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 no_std?

AFAICT the current implementation uses TLS to pass a Waker around but there's no TLS (or thread) support in no_std / core. I heard from @alexcrichton that it might be possible to get rid of the TLS if / when Generator.resume gains support for arguments.

Is the plan to block stabilization of async / await on no_std support being implemented? Or are we sure that no_std support can be added without changing any of the pieces that will be stabilized to ship std async / await on stable?

@TeXitoi

This comment has been minimized.

Contributor

TeXitoi commented Nov 30, 2018

@japaric poll now takes the context explicitly. AFAIK, TLS should no longer be required.

https://doc.rust-lang.org/nightly/std/future/trait.Future.html#tymethod.poll

Edit: not relevant for the async/await, only for futures.

@Nemo157

This comment has been minimized.

Contributor

Nemo157 commented Nov 30, 2018

[...] are we sure that no_std support can be added without changing any of the pieces that will be stabilized to ship std async / await on stable?

I believe so. The relevant pieces are the functions in std::future, these are all hidden behind an additional gen_future unstable feature that will never be stabilized. The async transform uses set_task_waker to store the waker into TLS then await! uses poll_with_tls_waker to get access to it. If generators get resume argument support then instead the async transform can pass the waker in as the resume argument and await! can read it out of the argument.

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.

@aep

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.

  1. see the "Poll" argument https://github.com/aep/osaka/blob/master/osaka-dns/src/lib.rs#L76 its an Arc
  2. registering something in the poll thing at Line 87
  3. yield to generate a continuation point at line 92
  4. call a generator from a generator to create a higher level stack at line 207
  5. finally executing the whole stack by passing in a runtime at line 215

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.

@Nemo157

This comment has been minimized.

Contributor

Nemo157 commented Dec 1, 2018

And I couldn't stop thinking about whether TLS-less async/await without generator arguments is possible, so I implemented a no_std proc-macro based async_block!/await! macro pair using just local variables.

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).

@Nemo157

This comment has been minimized.

Contributor

Nemo157 commented Dec 13, 2018

I just realized that there's no mention of moving await! from std to core in the OP, maybe #56767 could be added to the list of issues to resolve before stabilization to track this.

@Centril

This comment has been minimized.

Contributor

Centril commented Dec 15, 2018

@Nemo157 As await! isn't expected to be stabilized it's not a blocker anyways.

@cramertj

This comment has been minimized.

Member

cramertj commented Dec 17, 2018

@Centril I don't know who told you await! isn't expected to be stabilized... 😉

@crlf0710

This comment has been minimized.

Contributor

crlf0710 commented Dec 18, 2018

@cramertj He meant the macro version not the keyword version i believe...

@chpio

This comment has been minimized.

Contributor

chpio commented Dec 18, 2018

@cramertj

This comment has been minimized.

Member

cramertj commented Dec 18, 2018

@crlf0710 I did as well :)

@stjepang

This comment has been minimized.

Contributor

stjepang commented Dec 18, 2018

@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 await and await! possible? If we stabilize the macro, we'll never be able to remove it.

@cramertj

This comment has been minimized.

Member

cramertj commented Dec 18, 2018

@stjepang I really don't care too much in any direction about the syntax of await!, aside from a general preference for postfix notations and a dislike of ambiguity and unpronounceable/un-Google-able symbols. As far as I'm aware, the current suggestions (with ? to clarify precedence) are:

  • await!(x)? (what we have today)
  • await x? (await binds tighter than ?, still prefix notation, needs parens to chain methods)
  • await {x}? (same as above, but temporarily require {} in order to disambiguate)
  • await? x (await binds less tightly, still prefix notation, needs parens to chain methods)
  • x.await? (looks like a field access)
  • x#/x~/etc. (some symbol)
  • x.await!()? (postfix-macro-style, @withoutboats and I think perhaps others aren't postfix-macros fans because they expect . to allow type-based dispatch, which it would not for postfix macros)

I think that the best route to shipping is to land await!(x), un-keyword-ify await, and eventually someday sell folks on the niceness of postfix macros, allowing us to add x.await!(). Other people have different opinions ;)

@novacrazy

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 await! macro as it is and as it's described here: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

It's not any kind of magic or new syntax, just a regular macro. Less is more, after all.

Then again, I also preferred try! , as Try still isn't stabilized. However, await!(x)? is a decent compromise between sugar and obvious named actions, and I think it works well. Furthermore, it could potentially be replaced by some other macro in a third-party library to handle extra functionality, such as debug tracing.

Meanwhile async/yield is "just" syntactic sugar for generators. It reminds me of the days where JavaScript was getting async/await support and you had projects like Babel and Regenerator that transpiled async code to use generators and Promises/Futures for async operations, essentially just like we're doing.

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented Dec 18, 2018

Keep in mind that eventually we'll want async and generators to be distinct features, potentially even composable with each other (producing a Stream). Leaving await! as a macro that just lowers to yield is not a permanent solution.

@cramertj

This comment has been minimized.

Member

cramertj commented Dec 18, 2018

Leaving await! as a macro that just lowers to yield is not a permanent solution.

It can't permanently be user-visible that it lowers to yield, but it can certainly continue to be implemented that way. Even when you have async + generators = Stream you can still use e.g. yield Poll::Pending; vs. yield Poll::Ready(next_value).

@novacrazy

This comment has been minimized.

novacrazy commented Dec 19, 2018

Keep in mind that eventually we'll want async and generators to be distinct features

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 Futures as opposed to any regular value. An executor would be required to evaluate and wait on the async function to execute. Plus some extra lifetime stuff I'm not sure.

In fact, I once wrote a library about this exact thing, recursively evaluating both async functions and generator functions that returned Promises/Futures.

@rpjohnst

This comment has been minimized.

Contributor

rpjohnst commented Dec 19, 2018

@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 yield Poll::Ready(next_value), we want to yield next_value, and have awaits elsewhere in the same function.

@cramertj

This comment has been minimized.

Member

cramertj commented Dec 19, 2018

@rpjohnst

We don't want to yield Poll::Ready(next_value), we want to yield next_value, and have awaits elsewhere in the same function.

Yes, of course that's what it'd appear like to the user, but in terms of the desugaring you just have to wrap yields in Poll::Ready and add a Poll::Pending to the yield generated from await!. Syntactically to end-users they appear as separate features, but they can still share an implementation in the compiler.

@Pauan

This comment has been minimized.

Member

Pauan commented Dec 19, 2018

@cramertj Also this one:

  • await? x

@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/

“Async generators and iterators are what you get when you combine an async function and an iterator so it’s like an async generator you can wait in or an async function you can yield from,” he explained. Previously, ECMAScript allowed you to write a function you could yield in or wait in but not both. “This is really convenient for consuming streams which are becoming more and more part of the web platform, especially with the Fetch object exposing streams.”

The async iterator is similar to the Observable pattern, but more flexible. “An Observable is a push model; once you subscribe to it, you get blasted with events and notifications at full speed whether you’re ready or not, so you have to implement buffering or sampling strategies to deal with chattiness,” Terlson explained. The async iterator is a push-pull model — you ask for a value and it gets sent to you — which works better for things like network IO primitives.

@Nemo157

This comment has been minimized.

Contributor

Nemo157 commented Dec 19, 2018

@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 await syntax bikeshed again, but I have to respond to at least one point:

Personally I like the await! macro as it is and as it's described here: https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html

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:

  1. Hiding the underlying implementation, as one of the unresolved issues says you can currently create a generator by using || await!().
  2. Supporting async generators, as @cramertj mentions this requires differentiating between the yields added by await and other yields written by the user. This could be done as a pre-macro-expansion stage, if users never wanted to yield inside macros, but there are very useful yield-in-macro constructs like yield_from!. With the constraint that yields in macros must be supported this requires await! to be a builtin macro at least (if not actual syntax).
  3. Supporting async fn on no_std. I know of two ways to implement this, both ways require the async fn-created-Future and await to share an identifier that the waker is stored in. The only way I can see to have a hygienically safe identifier shared between these two places is if both are implemented in the compiler.
@cramertj

This comment has been minimized.

Member

cramertj commented Dec 19, 2018

I think there's a bit of confusion here-- it was never the intention that await! be publicly visibly expandable to a wrapper around calls to yield. Any future for the await! macro-like syntax will rely on an implementation not unlike that of the current compiler-supported compile_error!, assert!, format_args! etc. and would be able to desugar to different code depending on the context.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment