22 ms·
Await in Rust
- hansjorg 7y agoA bit confusing at first, but quite the novelty to use something like rustdoc and docs.rs as a blogging platform.
- roblabla 7y agoIt's become a bit of a tradition, with the new rustacean[0] being the first to do it I believe, as far back as 2015. rustdocs being just markdown with the convenient aspect of testing that your rustdoc actually works makes it a really good target for this kind of blogging! [0] https://newrustacean.com/show_notes/ https://newrustacean.com/show_notes/
- Narishma 7y agoI was confused as well. I thought it was the official Rust documentation until I read your comment.
- saghm 7y agoSlightly OT, but in case anyone else is curious, official Rust documentation lives at doc.rust-lang.org. docs.rs is auto-generated docs for packages published on crates.io.
- steveklabnik 7y agodocs.rs is run by the Rust team these days, so it is also "official", but documents un-official things.
- saghm 7y agoFair enough! "documents un-official things" is what I intended to imply; apologies if I wasn't clear
- steveklabnik 7y agoIt's chill! It used to be its own thing, and I'm not sure if people know that it's not or not anymore :)
- saghm 7y agoNow that I think of it, I'm not actually sure whether I knew before that it was official or not. I've definitely been using it for a long time now though, so I'm happy to hear it's in good hands!
- quazar 7y agoPeople in the Racket community also sometimes to use Scribble[1] to write their blogs. https://docs.racket-lang.org/scribble/ https://docs.racket-lang.org/scribble/
- jkoudys 7y agoI used to be a huge fan of async/await in the JS world, until I realized that we were tethering a very specific implementation to new syntax, without adding any clarity beyond what a coroutine and generator could do. eg `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for. I came in not wanting to see rs go down the same path. The idea of adding something that looks like it should be a simple value (`.await`) seemed odd to me, and I was already finding I liked raw Futures in rust better than async/await in JS already, especially because you can into a `Result` into a `Future`, which made async code already surprisingly similar to sync code. I will say, the big thing in rust that has me liking async/await now is how it works with `?`. Rust has done a great job avoiding adding so much sugar it becomes sickly-sweet, but I've felt `?` is one of those things that has such clean semantics and simplifies an extremely common case that it's practically a necessity in my code today. `.await?` is downright beautiful.
- matharmin 7y agoI've written a lot of JS/TS over the last couple of years, and I've found that even though it may just be syntactic sugar, async/await is a major win in both how easy it is to now write async code, and how readable the code is. Generators got us close to that, and it is a more generic feature. But over all the code I've written, I've only had one solid use case needing generators for something other than what's covered by async/await. Having a specific syntax that covers 95% of usages is very worth it in my opinion.
- jkoudys 7y agoLooking at the example I gave, how is `async function` any more clear than saying `co(function`? It covers the exact same use cases with equivalent clarity. They could've shipped the `Promise` object with a `coroutine` fn, like bluebird did, and had the same semantics without needing to reserve words and add syntax to the language. All it opens up is combining async and generators, which can be handy for iterating things like prefetched results, but that's a rare enough case not to need to be baked into the language. Separating the coroutine would let us work beyond just promises too. Wrap around old code using error-first callbacks, or new code using observables (yield to get the next, yield* to complete). I just wrote a coroutine to give quasi-concurrency to Google apps scripts, so it can queue up the routines running `fetch` methods and instead do them in one parallel-request-making `fetchAll`. It's a much better approach for the long-term of a language. Imho 90% of the junk we deal with in old languages is stuff for a convenient implementation at the time that we don't use anymore.
- autarch 7y agoAs a fairly new rustacean myself, I can confirm that the pre-syntax examples are incomprehensible gibberish to me. The bits with `await` are much simpler to make sense of.
- Scuds 7y agoYou'd be doing something similar with the task library in C# or Promises in Javascript (or in ES5-land, callback hell). I'd recommend just opening up an IDE in any of these and seeing the types and parameters that come in and out of these calls. The sync vs async worlds really to clash with each other but at least we have strongly typed languages to help with writing the code. In javascript you could be mixing callbacks and promises and encounter bits where you'd never know that the next line would never be executed because of some nonexistent callback, etc etc.
- Arnavion 7y agoYou'll still need to write enum state machines if you want to implement custom Streams (since libstd is only adding Future at the moment). Even with Futures there can be times when you need the fine-grained control over execution that you don't get with async-await. Eg if you want to implement a `struct TimeOut<F: Future> { inner: F, timeout: std::time::Instant }` from scratch, you can't do that with async-await.
- pimeys 7y agoNow being able to just dig into the lower level of Futures at work, I'd say even that is not that hard. You just write the `poll` function and then pattern match inside. There are even nice macros, such as the `ready!` that helps you with some of the boilerplate. The trickiest thing for me to grasp first was the `Pin` type. Like how does this prevent the value to be moved... Until I realized it's `Pin<&mut T>` and then I had my a-ha moment.
- Arnavion 7y agoIt's slightly more complicated than just pattern-matching. - In general you have to be careful that if you're returning Poll::Pending, it's after something has registered a wakeup, otherwise your future is going to just stop. - As a specific case, your match has to be inside a loop so that a state transition results in the new state being polled immediately. You can't just return and expect the caller to poll() you since there will be no wakeup registered that triggers the caller to do so. - Because futures receive `&mut Self` rather than `Self`, it's difficult to do a state transition where the new state needs to own some properties of the current state. Eg `State::Foo { id: String } -> State::Bar { id: String }` Instead you have to do Option::take / std::mem::replace shenanigans on the state fields, or have an ugly State::Invalid that you can std::mem::replace the whole state with.
- divan 7y agoReading through comments on async/await-related articles, I wonder if I'm the only person who find the whole concept of async/await utterly weird. Specifically I have troubles embracing the need to mark function as "async" – it just doesn't make sense to me. Synchronous or asynchronous is something up to the caller to the decide – not to the function itself. Like in real life I can do something synchronously (meaning paying full attention), or I can do the same task asynchronously (watching youtube videos in the background while cooking, for example), but it's still up to me. Being "async" is not a property of the "watching youtube" action, it's the property of the caller running this action. That's the reason why CSP-based concurrency models really works well for me – it's just so easy to map mental models of system's behaviour from the head directly to the code. You have function/process and it's up to you how do you run it and/or how synchronize with it later. Async/await concept so popular in modern languages is totally nuts in this aspect. Maybe it's just me, but I find myself adding more accidental complexity to the code just to make async/await functions work nicely, especially for simple cases where I don't need concurrency at all, but one "async" function creeps in.
- inglor 7y agoThat's 'lying' though, async/await is a great way to know which of your functions perform i/o and which do not. Namely in platforms like Node the convention is to not do synchronous I/O and as a communication tool - everything that doesn't use async/await is promised to never block or perform I/O. You might claim it's a lower level of abstraction but it makes concurrency a _lot_ simpler and it's why often when I write go I have to write 35 lines for something that would take me 5 in C#, Python, JavaScript or now Rust.
- divan 7y agoThanks, that's interesting point. I see how it might be useful, but it's still looks as a minor optimization that doesn't outweigh the drawbacks of the model. How do you deal with a situations when you have non-async/await code, and then you need to call just one function, which happened to be "async", and you can't do until you mark all your functions async as well? I find myself in this situation too often with Dart/Flutter, and it's especially embarrassing experience as it happens usually with a simple parts of code, where I don't need concurrency at all (like, get local directory name and store file to it). This "forced refactoring" also hits language design corners like "you can't call async functions" in constructor or `iniState()` and I end up writing async wrappers and other helpers functions just to make the whole thing work. > it makes concurrency a _lot_ simpler I find people put different meaning in the word "simpler". Do you mean that code requires less lines/chars to write or that it's easier to read and understand (those are totally different properties, often conflicting)?
- _bxg1 7y agoAm I the only one who finds it incredibly hard to grok how async/await and "regular" sequential code interplay? In JavaScript I strongly prefer the regular Promise way of doing things because async feels like it's mixing metaphors. Being relatively new to Rust it's not totally clear to me whether the Futures example in the article could be made more readable - it's definitely pretty gross - but I have to wonder.
- macromaniac 7y agoIt took me a while to get comfortable with it too. The intuition I use is that in the past it used to be DrawWindow(), DrawWindow(), DisplayResultsofSlowFunction() and then the window would freeze for a few seconds, but with async/await theres a queue basically and your DisplayResultsOfSlowFunction() gets put to the side while awaiting so DrawWindow() can keep doin what its doin and your app doesnt appear to freeze, for a gui example (idk if that helps or if ye already knew it but just in case, for me all the internet explanations never talked about the scheduler for some reason so it took me a while).
- chrismorgan 7y agoNote that futures, promises, &c. are all about deliberately asynchronous operations, most commonly I/O. Expressed in older terms, it’s cooperative multitasking. Thus, when you have a CPU-bound function, they are of no assistance in UI responsiveness, unless you deliberately insert yield points; in JavaScript, for example, you might do a unit of work, or process work until, say, ten milliseconds have elapsed, and then call setTimeout(keepGoing, 0). (Critically in JavaScript, you mustn’t use `Promise.resolve().then(keepGoing)`, because that’ll end up in the microtask queue which must be flushed empty before the browser will render, so you won’t actually yield to the UI with this approach.) So be careful about your DisplayResultsOfSlowFunction(). If its slowness is that it takes a long time to calculate, and it doesn’t actually have any asynchronous stuff in it, async will not help you.
- _bxg1 7y agoI understand how they work. It's just that turning something as fundamental as "each semicoloned statement happens after the previous one, as quickly as possible", into an abstraction, makes me really uncomfortable.
- MuffinFlavored 7y ago> Although the more ergonomic syntax is not ready yet, asynchronous I/O is already possible in Rust with ecosystem around futures, mio, and tokio. Does this mean all 3 of those crates are required to achieve async I/O? tokio is a large project. Which part of tokio specifically is supposed to be used in conjunction with futures + mio to achieve asynchronous I/O? Edit: it seems futures + mio are dependencies of tokio.
- steveklabnik 7y agoFutures are the interface for asynchronous code. You need some sort of executor to actually drive the chain of futures to completion. Tokio is the most popular option, but is not required, strictly speaking. You can whip up a simple executor with nothing but the standard library. It won’t perform as well as Tokio, of course. There’s a good reason it’s a big project. (Tokio is built on top of mio so if you’re using Tokio, you’re using mio)
- majewsky 7y agoDoesn't futures 0.3-alpha bring its own executor? I haven't followed Tokio too closely recently, but my understanding was that, going forward, futures provides the executor and Tokio provides the reactor. At least, that would be a division that would make sense to me.
- steveklabnik 7y agofutures-preview 0.3 does have futures::executor::ThreadPool; a simple thread-pool based executor. However, I thought that tokio did its own thing here. I could be wrong.
- Matthias247 7y agoThat is right. And while it theory having a separate executor (eg in the futures lib) and IO providers (timers, sockets) in another lib works it will in practice never be as efficient as having an integrated system like Tokio. The reason for that is that decoupled solutions can’t integrate the reactors in the same eventloop and there is always extra synchronization and thread hopping involved. The difference can be huge, eg 50% less throughout on a server.
- iddan 7y agoI'm building an async server application (https://github.com/iddan/minerva https://github.com/iddan/minerva) with async Rust and I feel the pain of not having await. Every time I'm using a Future I need to care about borrowing and Error types that if it was sync code I won't.
- j1elo 7y agoAs a pure C & C++ programmer, I was amazed of the (to my eyes) incredibly clever little trick that is the Duff's Device [0], and started using it right away to provide embedded programs with easy to write "concurrency" [1], [2]. Some time later I had to dip my toes into the world of Javascript, learning first about Promises, and finally reading about async/await. I just realized that it is basically the same trick I had been using all along. And now it's coming to Rust, neat! [0]: https://web.archive.org/web/20190217162607/http://www.drdobbs.com/a-reusable-duff-device/184406208 https://web.archive.org/web/20190217162607/http://www.drdobb... [1]: http://dunkels.com/adam/pt/ http://dunkels.com/adam/pt/ [2]: http://dunkels.com/adam/pt/expansion.html http://dunkels.com/adam/pt/expansion.html
- Tyr42 7y agoI don't see how Duff's Device provides "concurrency"? It's a way to unrolls loops. Now surplanted by SIMD instructions. Or do you mean state machines?
- jcranmer 7y agoI assume that what the OP meant by Duff's Device is "switch in C is syntactically a goto" rather than the specific loop-unrolling paradigm of Duff's Device itself. An example usage I've come across is the bzip2 decompressor: https://github.com/enthought/bzip2-1.0.6/blob/288acf97a15d558f96c24c89f578b724d6e06b0c/decompress.c#L195 https://github.com/enthought/bzip2-1.0.6/blob/288acf97a15d55... (note that the case statements are hidden in all of the GET_* macro calls).
- iherbig 7y agoLook upon the horrors and weep: https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
- j1elo 7y agoActually I meant "coroutines". You create a main infinite loop in your application, in which you call all functions that are meant to perform activities that should be happening "at the same time", like for example taking ADC measurements, while updating a PID control algorithm, while waiting for the GPRS chip to connect to the internet; those would be their own function, called "protothread" if you use the Adam Dunkel's library, and they are going to run a number of blocking operations, waiting for some external hardware to do their thing. Each one of those functions is converted into a Duff device by means of the macros provided by the Protothreads library. Now each protothread function can express its own operations in a linear and very clear way, and yield execution to the other functions by just returning from the Duff device. When the main loop starts over, each of those functions will "resume" their operation at the point where they yielded during the last iteration. The way I see it, this trick is much more than just loop unrolling; it provides a C developer with the same basic features that an async function would have in JS; and the yield points in the functions would be equivalent to the 'await' calls. This allows for very easy to read code, being able to follow the logic of each individual "thread" without much of the noise needed for taking care of the "coroutines" part. The next step (in the embedded world, at least) would be going the route of an RTOS, such as FreeRTOS, but I've managed well so far without it. EDIT: and yes, this is basically also a neat was of streamlining a state machine
- infinity0 7y agoYet another 1000-word rederivation of a monad, yawn. Wake me up when you've realised you might as well just write Haskell.
- steveklabnik 7y agoYou're missing the forest for the trees. While in some abstract sense, async/await is monadic, sure, Rust does not have the ability to express the monad abstraction, and it's unclear if it ever will be able to. Those details are what makes this topic interesting.
- carterschonwald 7y agofrom my previous interactions with the rust years ago and related rust design discussions on their issue tracker,the main barrier seemed to be rooted in the unfortunate "self" trick for how "the type im instantiating a trait with" syntax making higher kinded trait support expressible as a simple extension, This is of course ignoring the related need to also write code parametrized over lifetime variable structure ..
- infinity0 7y agoIt looks like you're missing the forest for the trees. I'm perfectly aware that Rust can't express monads.
- dang 7y agoHey, could you please not post in the flamewar style here? We're trying for something other than that on HN, including avoiding old-school programming language flamewars. It's been many years since any of those were fresh. https://news.ycombinator.com/newsguidelines.html https://news.ycombinator.com/newsguidelines.html
- bfrog 7y agoThere's been a long history of refinements leading to this. Fantastic work! Look forward to the async/await simplicity
- giovannibajo1 7y agoCan somebody explain the performance characteristics? For instance, let’s say I have a deep stack of 15 async calls. If the deepest call yields, what code is executed? Does it “jump back” to the top ala “longjmp” or it needs to return back up the stack frame by frame? And what happens when it resumes execution? IOW is the overhead linear with the stack depth or not?
- Rusky 7y agoYou are correct, the overhead is linear with the stack depth. I suggested a more longjmp-esque approach [here](https://internals.rust-lang.org/t/pre-rfc-cps-transform-for-generators/7120 https://internals.rust-lang.org/t/pre-rfc-cps-transform-for-...) early on, but it didn't seem to be worth the extra implementation effort. Benchmarking never showed this linear overhead to be an issue. The current thinking from the team seems to be that, if this does become an issue, a) inlining before state machine transformation can flatten this out quite a bit and b) convincing LLVM to do what would be required is a lot of work.
- Arnavion 7y agoAlso, you as the user always have the option of manually reducing the depth of the stack - spawn() a separate future with a one-shot channel sender and .await its receiver.
- Arnavion 7y ago>If the deepest call yields, what code is executed? Does it “jump back” to the top ala “longjmp” or it needs to return back up the stack frame by frame? It goes back frame-by-frame. Yielding (ie returning `Poll::Pending`) is essentially returning "I'm not ready yet" and it's possible the parent future wants to do something else in that case. The simplest case is a future that wraps a collection of futures and resolves to the value of the first one that resolves. In this case it would cycle through each child future and only return `Pending` if all of them return `Pending`. Another case is that of a timeout future that wraps another future - it would resolve to `Err(TimedOut)` if the wrapped future returns `Pending` for too long. >And what happens when it resumes execution? The top-most future of the task is all the executor knows about, so the executor has no choice but to start from there.
- deleted 7y ago[deleted]