3 ms·
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 t
by AndyKelley 8y ago
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 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...
- majewsky 8y ago> 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.)
- withoutboats 8y agoTo clarify, we don't use LLVM's implementation of coroutines. We have our own implementation in rustc.
- Rusky 8y agoOn 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); // ... } }