News for hackers. Stuff that matters.
Re:Async and Await in Rust: a full proposal by AndyKelley on Sunday July 15, 2018 @03:32PM (#17536638)
This is exciting. It allows programmers to model their problems in code that fully accounts for the parallelism.

For comparison, here is async/await in Zig: https://ziglang.org/documentation/master/#Coroutines

Zig decided to go the other way - when you async call a function, it does eagerly evaluate until the first suspend point. This is less overhead than immediately suspending, plus it removes the dependency of the language feature on a userland event loop. Users who want the immediate suspend feature can call a userland utility method of the event loop which suspends and then tail calls the async function in question.

Re:Async and Await in Rust: a full proposal by Rusky on Sunday July 15, 2018 @10:46PM (#17538415)
> when you async call a function, it does eagerly evaluate until the first suspend point. This is less overhead than immediately suspending, plus it removes the dependency of the language feature on a userland event loop.

This seems wrong. There's no more overhead for Rust's approach- which to be clear immediately suspends only the callee, not the entire stack of async functions. In fact, if you immediately await the future, the control flow is literally no different from eagerly evaluating until the first suspend point.

There is also already no dependency on a userland event loop. This is, again, because only the callee is immediately suspended. It's still entirely up to specific leaf future implementations to interact with (or not) the event loop.

Re:Async and Await in Rust: a full proposal by dom96 on Sunday July 15, 2018 @04:17PM (#17536833)
It's exciting to see so many systems programming languages implement async/await.

For a further comparison, Nim works in the same way as Zig when it comes to eager evaluation. Something that I'm particularly proud of when it comes to Nim's async/await implementation is that everything, right down to the macro which defines what `await` means, is implemented in the standard library. The compiler only implements the coroutines. This means that the language isn't bloated by this extra feature and makes it much easier for developers to implement their own async/await.

I believe there was talk to do the same in Rust, but for some reason the developers decided to implement it in the compiler instead.

Re:Async and Await in Rust: a full proposal by withoutboats on Sunday July 15, 2018 @09:02PM (#17538031)
We did implement it out of tree at first. [0] Because Rust requires annotations on macros to distinguish them from built in syntax, they are less ergonomic to use. In general, we believe in adopting a path of beginning as an out-of-tree macro, and then deciding to elevate that to first class syntax if its clear that they will be extremely commonly used.

[0]: https://github.com/alexcrichton/futures-await

Re:Async and Await in Rust: a full proposal by skybrian on Sunday July 15, 2018 @05:44PM (#17537271)
As another comparison, Dart started out delaying execution of async functions and then switched to eager in Dart 2:

https://groups.google.com/forum/#!topic/flutter-dev/3R9qhjNG...

Re:Async and Await in Rust: a full proposal by steveklabnik on Sunday July 15, 2018 @04:52PM (#17537024)
Async blocks require a keyword to be unambiguous with a struct constructor.
Re:Async and Await in Rust: a full proposal by aidanhs on Sunday July 15, 2018 @04:59PM (#17537051)
Can you clarify what youre referring to with "decided to implement it in the compiler instead", be it from this blog post or elsewhere?

Clearly you need coroutines in the compiler, and the way Rust treats ownership means pinning is needed in some way (I've not been following closely so don't know if this is in libstd or a language-level feature), but the rest (according to this post) seems to be going in libstd so you can write your own if you want.

Re:Async and Await in Rust: a full proposal by steveklabnik on Sunday July 15, 2018 @05:15PM (#17537123)
Pinning is almost entirely a library feature; it does use the unstable “auto traits” functionality of the language, so it’s a bit special in that regard. But it’s provided by the standard library.
Re:Async and Await in Rust: a full proposal by dom96 on Sunday July 15, 2018 @05:28PM (#17537184)
> Can you clarify what youre referring to with "decided to implement it in the compiler instead"

I'm referring specifically to the async/await syntax. Coroutines probably need to be implemented in the compiler, sure, but the actual async/await can be implemented with metaprogramming.

Re:Async and Await in Rust: a full proposal by kibwen on Sunday July 15, 2018 @06:57PM (#17537561)
Can you explain "this is less overhead than immediately suspending"? AFAICT Rust's approach is as low-overhead as it gets, since the inherent separation between future creation and future execution means that future creation doesn't need to have any cost at all. I'm also not sure it makes sense to refer to Rust's behavior as "immediately suspending", since it's not suspending anything: until someone chooses to start executing the async function, there's no state to suspend?

As for the rationale for why Rust is aiming for this behavior, I think this comment buried in the RFC discussion (https://github.com/rust-lang/rfcs/pull/2394#issuecomment-382...) sums it up (and makes note of Dart 2.0 as a contrasting example):

"A fundamental difference between Rust's futures and those from other languages is that Rust's futures do not do anything unless polled. The whole system is built around this: for example, cancellation is dropping the future for precisely this reason. In contrast, in other languages, calling an async fn spins up a future that starts executing immediately."

"A point about this is that async & await in Rust are not inherently concurrent constructions. If you have a program that only uses async & await and no concurrency primitives, the code in your program will execute in a defined, statically known, linear order. Obviously, most programs will use some kind of concurrency to schedule multiple, concurrent tasks on the event loop, but they don't have to. What this means is that you can - trivially - locally guarantee the ordering of certain events, even if there is nonblocking IO performed in between them that you want to be asynchronous with some larger set of nonlocal events (e.g. you can strictly control ordering of events inside of a request handler, while being concurrent with many other request handlers, even on two sides of an await point)."

"This property gives Rust's async/await syntax the kind of local reasoning & low-level control that makes Rust what it is. Running up to the first await point would not inherently violate that - you'd still know when the code executed, it would just execute in two different places depending on whether it came before or after an await. However, I think the decision made by other languages to start executing immediately largely stems from their systems which immediately schedule a task concurrently when you call an async fn (for example, that's the impression of the underlying problem I got from the Dart 2.0 document)."

Re:Async and Await in Rust: a full proposal by AndyKelley on Sunday July 15, 2018 @11:25PM (#17538533)
My understanding is that Rust uses LLVM coroutines[1]. In order to have the behavior where the function does not start executing immediately, you would follow the example that you can find by searching for "injected suspend point, so that the coroutine starts suspended". So what this looks like is:

* Function call

* The coroutine creates its frame (I believe this maps to "future creation".)

* Function return (the injected immediate suspend)

* (sometime later) Put the work on the event loop

* Function call (suspend resume)

* Function executes until a suspend point

Eager execution has no injected immediate suspend, so it looks like this:

* Function call

* The coroutine creates its frame

* The function executes until a suspend point (function return)

That's it. The coroutine is responsible for making sure that it gets resumed appropriately if it suspends. Await doesn't do anything with an event loop; it just suspends and then puts the suspended coroutine handle in the target coroutine's frame with an AtomicRMW. If it turns out the target coroutine already completed, then it grabs the result, destroys the target coroutine, and cancels suspending (which is a jmp instruction). Otherwise the suspend completes and the target coroutine will resume the suspended one when it completes.

> If you have a program that only uses async & await and no concurrency primitives, the code in your program will execute in a defined, statically known, linear order.

This is true with eager execution as well. You can see some examples here [2].

One more side note, how eager execution was valuable to me in the self-hosted compiler:

    pub async fn renderToLlvm(comp: *Compilation, fn_val: *Value.Fn, code: *ir.Code) !void {
        fn_val.base.ref();
        defer fn_val.base.deref(comp);
        // ...
    }
At the callsite we make an async call to renderToLlvm, passing in a ref-counted fn_val. If the body didn't execute immediately, the callsite might deref fn_val and destroy it before renderToLlvm gets a chance to add a reference, but because of eager execution, the ref is guaranteed.

[1]: http://llvm.org/docs/Coroutines.html

[2]: https://github.com/ziglang/zig/blob/363f4facea7fac2d6cfeab9d...

Re:Async and Await in Rust: a full proposal by majewsky on Monday July 16, 2018 @03:50AM (#17539487)
> My understanding is that Rust uses LLVM coroutines[1].

IIRC it doesn't. Coroutines would induce a significant cost (one stack allocation per future) which would make futures unsuitable e.g. for embedded systems.

My understanding is that Rust will compile futures into state machines. So a future would be a closure with a data object that's a sum type with one variant per suspend point, that contains all the variables and references that are in scope at that suspend point. (That's also why Pin needs to be added for futures/generators: This set of variables may be self-referential.)

Re:Async and Await in Rust: a full proposal by withoutboats on Monday July 16, 2018 @07:50AM (#17540517)
To clarify, we don't use LLVM's implementation of coroutines. We have our own implementation in rustc.
Re:Async and Await in Rust: a full proposal by Rusky on Monday July 16, 2018 @12:13PM (#17542443)
On top of the sibling comments pointing out that Rust doesn't use LLVM coroutines, which allocate each coroutine frame by default, you misunderstand the control flow of Rust async functions:

A Rust future is also "responsible for making sure that it gets resumed appropriately if it suspends." Rust's await is even cheaper than C++/LLVM's- it doesn't do anything with an event loop either; it just returns, without atomically messing with any coroutine handle (which doesn't exist).

Further, Rust futures are poll-based, not callback-based. Your bullet list looks like this:

* Function call to the main entry point (which is really more of a constructor)

* The future creates its frame as a value type (not as a heap allocation) and initializes it with the call arguments

* Function return (not a suspend, because it returns an initialized future rather than Poll::Pending- so again more of a constructor return)

* (sometime later, often immediately) Function call to `poll` (initial resume)

* `poll` executes until a suspend point (function return)

The initial call to `poll` can happen because the constructed future was put on the event loop and scheduled, as you describe, but it can also happen merely because its caller is also a future that is already running somehow.

Also, note that "the event loop" is a rather loose concept here- it's just "the thing that calls top-level `poll` functions." The language doesn't ever submit anything to it, or signal it, or anything. Each call to `poll` receives a handle to that top-level caller, and each leaf future stashes that handle somewhere that will signal it when it's ready to resume. This even works in embedded microcontroller scenarios.

And finally, your eager execution example is addressed in Rust in two ways:

* First, ownership and the borrow checker prevent dangling references like this statically. In your particular case, the caller would increment the refcount as part of cloning an `Rc`, and then move the clone into the future. (Unless, of course, the future was short-lived enough, and you decided to take advantage of that to pass in a `&'a Value.Fn` instead.)

* Second, because future construction (the first three bullet points) and future execution (the last two) are decoupled, you can write your own function that does any extra construction work in the cases that it's actually necessary. For example, using Rust's syntax for async blocks:

    pub fn render_to_llvm(.., fn_val: *Value.Fn, ..) -> impl Future<Output = ()> {
        fn_val.base.ref();
        async {
            defer fn_val.base.deref(comp);
            // ...
        }
    }