9 ms·
Inside Rust's Async Transform
- benaadams 8y ago> 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
- zamalek 8y agoYou can use ILSpy and disable async decompilation to see that C# creates a state machine.
- the_grue 8y agoSame for Kotlin.
- Nemo157 8y agoTried to clarify this, it's not that C# and JS are implemented as CPS-like, rather that that's how they're commonly explained; and because of the GC this difference is not really externally visible, whereas with borrowing in Rust it would be.
- Matthias247 8y agoYes, 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.
- paulddraper 8y agoIDK about JS runtimes, but this is very similar to the approach by most JS transpilers. They will use language-level generators if compiling to ES 2015, or user-land generators if compiling below that.
- orf 8y agoDoesn't both JS (via Babel) and C# implement asynchronous functions as state machines in a similar fashion?
- leshow 8y agoAccording 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
- aaaaaaaaaab 8y agoYes: https://sharplab.io/#b:master/K4Zwlgdg5gBAygTxAFwKYFsDcAoUlaIoYB0AKgBYBOqAhgCb5k0gDWIO2ADsAEYA2YAMYxBfZiBgBhGNgDe2GIpjd+QmMwQRhpZixgBZABQBKGUpjzzAX2xWgAA= https://sharplab.io/#b:master/K4Zwlgdg5gBAygTxAFwKYFsDcAoUla...
- steveklabnik 8y agoOne 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.
- chusk3 8y agoNo 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.
- BonesJustice 8y agoThat’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.
- deleted 8y ago[deleted]
- leshow 8y agoI 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 8y agoThe 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.
- FridgeSeal 8y agoOff 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 8y agoOnly 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 8y agoGosh I wish more websites were this minimal. It's refreshing to use something so streamlined.
- Nemo157 8y agoWhat cokml19 said, it’s all about minimising the work. This is just a normal Jekyll site hosted on github pages, but with just minimal css and js inlined into the page. There are actually a few more lines of js hidden around the place, e.g. for adding the “play” buttons onto the code snippets, but it’s all simple library-less code.
- 2bitencryption 8y agoAsync/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?
- lukasLansky 8y agoIt would be misleading for consumers of the interface to mark compute_pi_digits awaitable -- at least in .NET world that have quite a lot of experience with async-await at this point. See http://blog.ploeh.dk/2016/04/11/async-as-surrogate-io/ http://blog.ploeh.dk/2016/04/11/async-as-surrogate-io/ for further discussion. Regular programmers are not using Task<T> as IO-monadic marker consciously, but they are surprised when a usage differs from that model.
- int_19h 8y agoLook into continuation-passing style. Semantically, async/await is much like syntactic sugar for CPS (or rather futures, but at the most basic level they can be thought of as single-shot continuations). But ultimately, to make use of async, you need async primitives - something that lets you say "do this in the background somehow, and let me know once you're done". Any async/await call should ultimately end at one of those primitives, and it's at that point that another call might get interleaved. If you don't actually do I/O or anything else that can do a non-blocking wait, you're not getting anything useful from async.
- deleted 8y ago[deleted]
- sifoobar 8y agoI'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 8y agoThe 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 8y agoAnother 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 8y agoSure, 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.
- throwaway487552 8y agoWhy they are ignoring Erlang's and Go's much more saner approach with blocking lightweight processes/goroutines which just work? It is not just better, closer to reality semantics (someone have to wait), but also better granularity (one connection/channel, one process), shorter, less confusing code. My guess is that the choices were made on the basis on meme/popularity among ignorant coders (to attract more devs, to gain more momentum, etc) rather than tech per se. To have a select and receive as a language constructs (with pattern matching, of course) and coroutines (basically, software interrupt handlers) is a much better approach for a system programming language, just by being close to a system. Universal frameworks like tokyo or async/await is cancer and , it seems, does not suit a system language at all.
- steveklabnik 8y agoThose lose zero-overhead interop with C, which is a core constraint for Rust. Your guesses are incorrect.
- throwaway487552 8y ago> lose zero-overhead interop How many LOC is tokyo nowadays?
- rokob 8y agoTokyo is a city
- steveklabnik 8y agoI don’t understand how that’s relevant.
- throwaway487552 8y agoTo me, at least, this means that the overhead is substantially greater than zero.
- anonbluecc 8y agowow... talk about informative.
- deleted 8y ago[deleted]
- fulafel 8y ago> 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)
- jimbo1qaz 8y agoBikeshedding: I think Solarized light/dark color themes have insufficient contrast ratios.
- sriku 8y ago(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 8y agoWhy this instead of "green threads"? Runtime overhead/footprint?
- steveklabnik 8y agoYep.