
Inside Rust's Async Transform - Nemo157
https://blag.nemo157.com/2018/12/09/inside-rusts-async-transform.html
======
2bitencryption
Async/await pattern always confuses me, someone please let me know if I get
this right:

First, async/await does NOT mean "threading" or "multiprocessing" or
"concurrency". It simply means "using a state machine to alternate between
tasks, which may or may not be concurrent." Right?

Further, in Javascript, futures and async are utilized heavily because we so
frequently need to wait for IO events (i.e.: network events) to complete, and
we don't want to block execution of the entire page just to wait for a IO to
complete. So the JS engine allows you to fire off these network events, do
something else in the meantime, and then execute the "done" behavior when the
IO is complete (and even in this case, we might not be concurrent, because ).

That makes sense to me.

But say I have written something in Rust that makes use of async/await. And
say there is absolutely no IO or multithreading. Say I have some awaitable
function called "compute_pi_digits()" that can take arbitrarily long to
complete but does not do IO, it's purely computational. Is there any benefit
to making this function awaitable? Unless I actually spawn it in a different
thread, the awaitable version of this function will behave identically to if
it were NOT awaitable, correct?

And one last idea: the async/await pattern is becoming so popular across
vastly different languages because it allows us to abstract over concepts like
concurrency, futures, promises, etc. It's a bit of a "one size fits all"
regardless of whether you're spinning up a thread, polling for a network
event, setting up a callback for a future, etc?

~~~
skybrian
Incremental computation can be useful for better responsiveness even if you
only have one thread. A simple example in JavaScript would be a Mandelbrot
viewer, where you don't want to lock up the UI doing a heavy computation. So,
you could have something that looks like an async call that really does the
heavy computation in small chunks using idle callbacks, and the future
completes when it'd done. (Using a background worker thread is probably better
though.)

The async call itself doesn't return intermediate results, though, so you'd
have to handle that a different way. And if you want to cancel the task, you
need another way to handle that too.

Something like computing the digits of pi would be better represented by a
stream or iterator since the caller should decide when it's done.

~~~
jacquesm
The correct way to deal with that problem is to decouple the UI from the
computation, once process for each would be the ideal, anything less is going
to messy and require all kinds of hacks to give the same outward appearance.
Better still: one supervisor process and one for UI and computation each.

~~~
skybrian
Well, in a web app you don't get to make those choices, but you could use a
web worker.

------
benaadams
> is very different to other well-known implementations (C# and JavaScript
> [...]). Instead of 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, Rust instead uses a generator/coroutine transform to
> turn the function into a state machine.

C# async/await is also very much resumable state machines

~~~
Matthias247
Yes, the state machine generation aspect is similar.

However the execution aspect is a bit different: In C#, once a leaf
future/Task gets resolved, it will in many cases sychronously call back into
the state machine which awaited the task Task (by storing a continuation
inside it). A whole promise chain might resolve synchronously directly on the
stack of the caller. And "in many cases", because the whole thing depends on
some very subtle properties like whether a SynchronizationContext or
TaskScheduler was configured.

In Rusts task system a leaf future will never call back into the parent. It
will always only notify the associated task executor, that it can retry
running/polling the Future to completion. When the task gets executed again,
it will run again from the normal scheduler thread in a top-down fashion.

This makes Rusts system a little less performant for some use-cases, but also
a lot less error-prone (no synchronization issues because it's not known where
some code runs). It also is one of the key ingredients for avoiding
allocations on individual futures.

Javascripts system is closer to the C# mechanism, but avoids the error-prone
part: When a leaf future is finished, it will lead to calling the continuation
of the parent future. However this is never done synchronously, but always on
a fresh iteration of the eventloop (to avoid side effects). That works fine
for Javascript because the eventloop is guaranteed (it's not in C# async
code), and Futures are on the heap anyway.

------
FridgeSeal
Off topic, but I’d just like to point out how blindingly fast this site loads:
it loads quite literally instantly for me (I’m on mobile so I can’t give
precise figures) but I don’t think I’ve ever used a site that loads that fast
ever before.

Is the website author here? What are you running server side that’s giving
such great performance?

~~~
cokml19
Only two requests: favicon and html page itself. No front-end framework, no
tracking. The only JS is the 10 lines necessary for the buttons in the upper
right corner.

~~~
FridgeSeal
Gosh I wish more websites were this minimal. It's refreshing to use something
so streamlined.

------
leshow
I got lost in the weeds fairly quickly with this blog post, why is it that you
didn't have to implement Future?

Pinning is required here because your AsyncRead read_to_end returns a future
bound by some reference lifetime?

~~~
Nemo157
The actual Future implementation comes from std, std::future::from_generator
takes a generator with the right associated types and turns it into a future.

Yep, the generator created by quote_encrypt_unquote is creating internal self-
references from the future created by read_to_end into the AsyncRead it's
storing in its environment, while this is happening the AsyncRead must not
move and therefore the generator must not move, which is what pinning
represents.

------
orf
Doesn't both JS (via Babel) and C# implement asynchronous functions as state
machines in a similar fashion?

~~~
steveklabnik
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.

~~~
skybrian
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...](https://www.reddit.com/r/rust/comments/8aaywk/async_await_in_rust_a_full_proposal/dwxjjo2/)

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

~~~
Matthias247
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.

------
fulafel
> Instead of thinking of a CPS-like transform where an async function is split
> into a series of continuations that are chained together via a Future::then
> method, Rust instead uses a generator/coroutine transform to turn the
> function into a state machine

State machines are also what Clojure(Script) core.async uses.

(Easy choice as there are no continuations available)

------
sifoobar
I'll take fibers that yield automatically on blocking operations over
async/await most days for most tasks. It's slightly less flexible, since you
can only wait for one async action at a time per fiber; but a pleasure to use
in comparison. But for that you need fibers built in.

Go sort of does the same thing, but insists on running fibers in separate
threads at its convenience; which means giving up the lovely simplicity of
cooperative multitasking for the same old multi-threaded circus.

I'm unfortunately not aware of any languages more recent than Smalltalk that
get this right. My own baby, Snigl [0], is just getting to the point where
it's doable.

[0] [https://gitlab.com/sifoo/snigl](https://gitlab.com/sifoo/snigl)

~~~
int_19h
The problem with fibers is interop. The moment you need to do some FFI,
especially FFI that involves callbacks, things get a lot more complicated,
since code you're calling into/through doesn't have any of that fiber magic
(and, depending on how you implemented yours, it might actually break it).

~~~
sifoobar
Another win for embedded languages/inside-out FFI in my book, since
controlling the outside world (C in Snigl's case) makes it trivial to register
a trampoline with whatever state needed to deliver the call.

~~~
int_19h
Sure, but now you need C code that needs to be aware of your trampoline,
specifically. Good luck if it's an existing library. Also, what happens if
there are multiple interleaved C parts of the stack, and the innermost one
invokes the trampoline? What happens to the ones in the middle? It all sounds
awfully like setjmp/longjmp (which is the one thing that you never do in C if
you want to interoperate with anything in a sane fashion).

And I don't think FFI direction matters much. The moment you have callbacks,
your stack has interleaving of languages anyway (i.e. X called into Y which
called back into X). Does it really matter which language the innermost and
the outermost stack frames belong to? You still need to handle the mix in the
middle.

~~~
sifoobar
Why? You really only need the C library to be able to pass a data pointer to
the callback, and most do.

This sounds like a native compiler perspective to me; with pure VM fibers like
Snigl's these are not issues.

It's not about direction, it's about controlling the world from the outside.

You sound more like you're on a mission to prove to the world it's impossible,
since Rust didn't manage to get it right.

~~~
int_19h
What happens with all the interleaving stack frames when that callback gets
invoked?

------
sriku
(shameless plug) The state machine approach is one I used in an old sweetjs
library called cspjs[1] which implemented async tasks into JavaScript well
before async/await. I still think there are some good ideas left there -
especially async error handling and native "data flow variables".

[1] [https://github.com/srikumarks/cspjs](https://github.com/srikumarks/cspjs)

------
networkimprov
Why this instead of "green threads"? Runtime overhead/footprint?

~~~
steveklabnik
Yep.

------
jimbo1qaz
Bikeshedding: I think Solarized light/dark color themes have insufficient
contrast ratios.

------
anonbluecc
wow... talk about informative.

