Hacker News new | ask | show | jobs
by orf 2748 days ago
Doesn't both JS (via Babel) and C# implement asynchronous functions as state machines in a similar fashion?
4 comments

Yes, Babel's transform of `async` to ES5 is similar. It turns has to turn a function into a state machine. Rust goes a step further and turns the entire chain of futures into one large combined state machine.

But the rest of the event-loop machinery is quite different. JS's async is still fundamentally callback-based. Rust's futures are polled. In JS there's a single global event loop and promises run automagically. In Rust you create futures managed by their executors, each handling its own kind of tasks (CPU pools, network polling).

Thank you for the detailed explanation!

Would you mind elaborating on the polled aspect of rust futures, or link me to some documation? Do you mean that there is a loop polling the result of a future? How does that work with things like select?

Futures do nothing until the poll method is called. That method returns an enum, saying if it’s ready, pending, etc. the event loop calls poll repeatedly.

We have some really in depth docs in the works here but it’s not quite ready yet.

Yes, the executor is that loop. Here is an older blog about Rust futures that is still relevant: http://aturon.github.io/2016/09/07/futures-design/
Could generating a single combined state machine result in code bloat? (Similar to inlining.)
The state machine here is a struct given to `poll()` methods that advance the state. It's similar to iterator structs that have `next()` calls.

It could cause code bloat, but in practice these are small functions that get inlined and optimized out to almost nothing (sometimes even the whole struct disappears).

One difference that may exist is that in Rust, async fns don’t immediately execute, they simply create one of these values. I forget if JS and C# do something different, that is, the execute up until the first suspend point. This was one of the major design decisions we’ve made that’s different than other languages.
Just for comparison, Dart started out with async functions that suspended immediately, but in Dart 2, they switched to running synchronously to first await for performance (fewer unnecessary suspends) and to avoid race conditions.

Without this, you sometimes had to write a write a wrapper function that does some synchronous setup and returns a Future, which was a bit annoying for stylistic reasons.

There's an interesting but somewhat old discussion here:

https://www.reddit.com/r/rust/comments/8aaywk/async_await_in...

I wonder if anything changed since then? I'm not a Rust programmer so I didn't really understand the article.

Immediately executing in Rust doesn't work very well for a few reasons. One of them is "pinning". In order to "start" a future, it must be pinned to a certain memory location and must never be moved from there anymore. If a future would start directly from the call which returns it, it wouldn't be possible to move the Future anymore. E.g. return it from another function, store it in a Vec<Future>, etc.

I found out it works also better together with some other features, like "select!" and the current cancellation mechanics. But I can't remember all the details right away, and it might be pretty hard to explain.

That said the choice makes sense for Rust for those reasons, but is not a general one! I think for Javascript, C# and Dart synchronously executing until the first await point is easier to understand. I e.g. felt that "async" in F# was a bit harder than in C# due to the non immediately executing property.

A Rust async function doesn't work like Dart 1- it doesn't suspend immediately (i.e. return back to the event loop to be scheduled), it just doesn't run to the first await until it's actually awaited or otherwise `poll`ed itself.

So the performance concern simply does not apply- Rust suspends the same number of times as Dart 2. The race condition concern might, depending on how you look at it, but in return the execution model from within a Future is much more straightforward and predictable.

Yes, that was a big bit of feedback I was very happy to see.

I don’t 100% remember, but I think some of the details for us still ended up significantly different. There are so many ways to implement this stuff...

No you're correct, C# async/await synchronously executes up to the first yield point. In general the tasks are 'hot' in C#, as opposed to F#'s 'cold' Async type.
That’s true for `async` methods, but keep in mind that you can `await` any value in C# that implements the ‘awaitable’ pattern. A custom ‘awaitable’ can perform its own scheduling however it likes, including when it begins its execution.

The pattern-based implementation of `await` is, in my view, the coolest part of the async/await feature set.

C++ follows up on the same idea.
Though F# Async is quite a bit different in that it is the computation rather than a cold invocation. One can use Tasks in F# of course but last time I checked, it will only use a CPS transformation rather than the more efficient state machine representation C# has (F# does use state machines for seq expressions so it’s not a fundamental limitation but more a matter of work).

Aside: This can be a source of errors in F# code I’ve seen as folks will mix up the cases where they want to result vs running a computation many times. There certainly is plenty of expresivity but in practice it’s a muddier representation (it took me over a year to appreciate this).

JS starts immediately but even if the function's return value is "ready" synchronously, you can't get its value back synchronously.
I've never thought about the execution order before but that makes a lot of sense. Thinking about it now, an async function is just sugar for wrapping the return in a Promise (besides the async/await restructuring). And constructing new Promises works that way exactly.
According to this blog post, no:

"... performing a CPS-like transform where an async function is split into a series of continuations that are chained together via a Future::then method"

they are referring to c# and js implementation of promises/futures here