6 ms·
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: ht
by AndyKelley 8y ago
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 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.
- dom96 8y agoIt'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.
- steveklabnik 8y agoAsync blocks require a keyword to be unambiguous with a struct constructor.
- aidanhs 8y agoCan 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.
- steveklabnik 8y agoPinning 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.
- dom96 8y ago> 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.
- skybrian 8y agoAs 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/3R9qhjNGZk4 https://groups.google.com/forum/#!topic/flutter-dev/3R9qhjNG...
- withoutboats 8y agoWe 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 https://github.com/alexcrichton/futures-await
- kibwen 8y agoCan 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-382399020 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)."
- AndyKelley 8y agoMy 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 http://llvm.org/docs/Coroutines.html [2]: https://github.com/ziglang/zig/blob/363f4facea7fac2d6cfeab9d1d276ecd8e8e4df0/test/cases/coroutines.zig#L26-L34 https://github.com/ziglang/zig/blob/363f4facea7fac2d6cfeab9d...
- Rusky 8y ago> 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.