Hacker News new | ask | show | jobs
by steveklabnik 2755 days ago
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.
3 comments

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.