Reading 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.
> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.
async/await aren't strictly necessary, but to avoid them you need one of:
1. a sufficiently smart compiler with whole program compilation, or
2. to compile two versions of every function, an async variant and a sync variant, or
3. every async wait captures the whole stack, thus wasting a lot of memory.
Async/await is basically a new sort of calling convention, where the program doesn't run in direct style but in continuation-passing style. This permits massive concurrency scaling with little memory overhead, but there other tradeoffs as per above.
Async/await makes perfect sense for Rust which wants to provide zero-overhead abstractions.
This is all just down to a poor choice of words, and maybe syntax.
The choice of `async`-vs-not is not the one you describe. What we label with `async` is a particular compilation style that makes the function interruptible in userspace, in exchange for making synchronous and recursive execution a bit more complicated. This style is important because it gives you event loop-style concurrency without the performance costs of threads (kernel or userspace/green/etc).
However, this compilation style still leaves the choice you describe up to the caller. Most languages seem to reverse the default choice syntactically, but you can still synchronously block on a call to an async function or run it concurrently with something else. It just so happens to be useful primarily for doing the latter, so it gets lumped in with it. (And we get unfortunate misunderstandings like "What Color is Your Function?" that miss this point.)
As an example of a language that doesn't reverse the defaults, look at Kotlin's `suspend fun`s. While they still have `async`-like callee annotations to control the compilation style, a simple function call behaves the same regardless of the callee, and you instead use various `spawn` or CSP-like APIs to get concurrency.
If changing the keyword and making the defaults match threads/CSP isn't enough, perhaps viewing the annotation as part of an effect system would help? The thing being tracked here is "this function can suspend itself mid-execution," and a good effect system even functions be polymorphic over things like this. For example, an effect-polymorphic function passed a closure or interface can automatically take on the effects of its callees, simplifying the program somewhat if you're faced with viral async-ness.
I can confirm there are at least 2 of us. I suspect a few more given previous discussions on the topic, e.g. [0].
Steve Klabnick provided some useful context on that thread for the Rust decision specifically. More generally though, you've nailed my primary misgiving:
> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.
Erlang/Elixir and Go place the decision with the caller, not the callee. That just seems much more sensible.
An alternative is perhaps to see async/await as dataflow going through the awkward teenager stage. Maybe one day we'll wake up and languages in general will have fully embraced dataflow variables as first order constructs.
Thing of it as a different analogy. There are various chores to do around the house - some of them must be done synchronously (do the ironing - we don't want to leave the iron on). Some of them can be done asynchronously (run the washing machine). If you want to write out your tasks it might look something like this.
LoadWasher() (sync)
washerRunning = RunWasher() (async)
var cleaning = CleanKitchen() (async)
wetClothes = await washerRunning;
LoadDryer(wetClothes)
clothesToIron = await RunDryer() // while we're awaiting we walk back up the stack and can continue cleaning the kitchen until the dryer is finished
IronClothes(clothesToIron) // this is just happening synchronously
await cleaning //last job to do so we need to make sure we finish!
Don't know if that helps, but I think it's an interesting analogy
That'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.
If I call you on the phone, I expect a synchronous response.
If I text you, I expect an asynchronous response. It's ok if you get back to me 3 hours later (well, context-dependent).
If I call you, ask you a question, and then you wait 3 hours before giving me a response, I've been sitting there blocked for 3 hours, unable to get any more work done, because I asked for a synchronous response and got an asynchronous one. Even worse, if you try and give me the response over text, I won't see it because I have the phone pressed to my ear and now I'm blocked forever.
---
The caller and callee have to agree on whether the communication is synchronous or asynchronous. Asking for synchronous and getting asynchronous doesn't work. Asking for asynchronous and getting synchronous kind of works, but that's not truly synchronous, that's just asynchronous with zero delay before getting the response. Or even worse, asking for asynchronous (and giving you a completion handler to fire) and you fire it synchronously before returning control to me, that way lies madness. Don't do that.
EDIT: actually this was wishful thinking, since both functions would block the main thread. The functions need to be modified to "suspend" for this to be truly async.
My ideal language would be something like Kotlin suspend semantics, except ALL functions were implicitly "suspend" functions. If that's even possible...
I am a huge Rust fan, but couldn't really articulate why async/await seemed so complicated. You have nailed it right on the head!
This is a exactly how I feel as well and I write Rust code every day.
But no matter, I will suck it up. The language is evolving and I think async/await has, seemingly, been one of the biggest divisive feature the community has faced. I will still use as there are so many other benefits this language brings.
I think of async to mean "When you call me I won't actually do anything right now, I'll just prepare the data structures to help remember what needs to get done, and return those structures to you, which you can then hand over to an event loop (executor) at your leisure". That return type? That's not actually what is getting returned right now when you call the function... that's what the future returned will eventually resolve to. That level of unexpectedness/magic is hard for me to stomach but in this case I'm ok because the feature provided and the ergonomics are so very good.
I think of await to mean "When this code is combined with all the other code that needs to be executed asynchronously on the event loop, this code will not be able to progress until this thing we are waiting on finishes. The event loop can therefore use this hint to schedule things and keep the processors busy."
I think I built up a good understanding of how this kind of a system works with futures 0.1 and tokio. It took some time but it all clicked together for me. But as for async/await as language features I'm satisfied to just let it be magic. I don't care to look under the covers at this point. I know it's similar to how futures worked, and I trust the rust team weaved it together well.
I agree. I raised the topic recently on HN here [1], but didn't get much of a response.
One of the problems with async is that it's viral. Make a function async and your entire call graph has to be made async, unless they handle the future manually without await. Async introduces a new "colour" to the language.
I absolutely agree that asynchronicity should be the provenance of the caller. If you look at typical code in languages with async/await such as JavaScript and C#, awaiting is the common case. So if async causes your code to be littered with awaits anyway, it makes more sense to make awaiting the default (just like it's the default for the rest of the language) and "deferring" the exception. Call it "defer" or something.
(As an aside, I was always disappointed that Go's "go" doesn't return a future, or that indeed there's no "future" support in the standard library. Instead you have to muck about with WaitGroup and ErrGroup and channels, which introduces sequencing even in places that don't need it. Sometimes you just want to spawn N goroutines and then collect all their results in whatever order, and short-circuit then if one of them fails. The inability to forcibly terminate goroutines is another wart here, requiring Context cancellation and careful context management to get right.)
I completely agree. In Java we're taking a different approach: https://youtu.be/r6P0_FDr53Q (my talk also explains why this option may not be available to C/C++/Rust)
Leaving it up to the caller to decide seems reasonable, but you only have control over the top level, you still have no control over how the function you're calling calls other functions.
Also, blocking on a future is trivial (just call poll).
So I would argue that async/await does a better job of leaving it up to the caller to decide: use await if you want async, use poll if you want sync.
As I've always said, you can use CSP in Rust. Just use threads and channels. They work great, and they even scale well on Linux to all but the absolute most demanding workloads. If you want a Go-like M:N scheduler, we've got that too: use mioco.
CSP as implemented in languages like Go is just threads, but with an idiosyncratic implementation.
Whole-heartedly agree. I strongly believe that async/await will be acknowledged as a design-mistake a decade from now. Async should be caller and not callee controlled - it also makes the implementation simpler and reduces maintenance.
I'd rather mark a function as "async" than to constantly do the mental gymnastics of whether a function is going to run synchronously or asynchronously, or even worse, always assuming that it'll run asynchronously and always have to deal with callbacks / triggers and the like just to get things to execute in order.
> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.
This doesn't work with Rusts implementation, because the functions have slightly different semantics and limitations.
E.g. an async function can't be recursive, since that would lead to a Future type of infinite size.
Async functions also don't necessarily run to completion like normal functions. They can instead also return at any .await point, which are implicit cancellation points. That means if someone just adds an async modifier to a function and some .await calls to methods inside it the result might not be correct anymore, since not all code in the function is guaranteed to be run anymore.
The cancellation mechanism was an explicit choice of the design - it would probably have been possible too to make async functions always run to completion and to thereby avoid the semantic difference.
Your thought that it's the caller makes some sense. Think of the OS level call to read a file, you can either ask for it to block until end of line, or just give you what it has now, based on a flag.
But "wait until there's eol, than call this callback" or its cousin the future which is "when complete, read here to find the callback(s) to call" which let's you fill in the callback after the call, that's inherently an async call. Same way sending a letter forces you to wait. Sure you could stand by the mailbox until the reply arrives, but that's just converting the async call to synchronous by waiting.
Though I don't really see how futures are that different from a cps, especially if you view them as chaining .then calls, which are exactly continuations. Await just let's you drop some syntax, and unify scopes.
No, a synchronous procedure is a procedure whose minimum completion time depends solely on the speed of the CPU and memory system (and lock contention, in some models), while an asynchronous procedure is a procedure whose completion time also depends on external events.
In other words, synchronous procedures are essentially locally CPU-bound, while asynchronous procedures are bound by I/O, network or IO/network/CPU of remote servers.
Async functions are the most efficient way to support asynchronous procedures, while non-blocking non-async functions are the most efficient way to support synchronous procedures.
There are also adapters to turn synchronous function into asynchronous ones, and to turn asynchronous functions into blocking non-async functions, although they should only be used when unavoidable, especiallly the latter.
I totally agree with you, however the alternatives to Async/Await are just way more wierder IMHO. Promises, Futures and what not. I am not very familiar with CSP based concurrency but Async-await strikes a good compromise to me. I just can't deal with this Promise/Future/Callback hell when it comes to managing multiple asyncrhonous or dependent asysnchronous calls. I think Chris Lattner does a way better job at explaining this -
Fibers are way better than async. Go has them and Java is working on it with project loom. It's a slight performance disadvantage (maybe 2%) but light-years easier to work with.
Languages with fibers figure ou execution suspend points and physical core assignment for you and abstract it all away. So physically they're doing the same thing as async, just without all the cruft.
Rust decided not to go with fibers to avoid having a runtime. I still disagree with this because they already do reference counting, not every worthwhile abstraction is zero cost.
Perhaps it's because you can easily bolt on CSP to the languages where it's popular? JS is single threaded, and Rust and C# don't have green threads.
I consider Rust to be an antithesis to your remarks because it has realoy good tools to prevent concurrency issues and yet still wants it.
I think on a deeper level it's because there are times where the thread isn't really doing anything and is instead waiting, and that time could be better apent doing something else. That requires sync, either with callbacks, promises or sync/await.
Well, practically it is for the caller to decide, you can `await` on a function (use it as if it were sync), or don't, grab it's `Future<something>` result and work with that (async).
Now `async`, you could see it as an implementation detail that you need in order to get much better performance...
Threaded IO is a low level thing there because of overhead caused by OS threads and Rust is a low level language with abstractions to deal with such low level interfaces. Async/await is better than callback passing - looks like normal call flow - but you need to know what's happening underneath. Zero cost abstractions. If you understand the implementation details it makes perfect sense - otherwise you can just keep doing the old way (callback pyramids or blocking IO)
If you don't have async markings, you can't know where in your code you have concurrency and this would require you to use synchronization primitives. For shared memory languages this means all that error-prone shared memory multithreading, like in Go [1].
You can run an async routine synchronously if you use the correct runtime, or block while polling until the function succeeds... "async" functions are functions that can be suspended and turned into an opaque thunk that you can pass around as Rust data, and "synchronous" functions aren't. If you like, you can think of async functions as being functions that can run either synchronously or asynchronously.
You can't (in safe Rust) easily replicate what async does for you because it understands how to handle borrows across yield points, which turn into self borrows when you turn the stack into a concrete object in Rust. That's not really an issue in garbage collected languages so it's not quite as much of a necessity to have a specific keyword, but it's still inconvenient in most languages to write everything in CPS or use combinators to acquire thunks.
As for why you would want a function that's not asynchronous... well, various reasons, but one is performance. Creating a thunk and then sending it to a runtime which decides what to do with it (or polling) usually has some overhead compared to linearly calling a function on the stack.
Another reason is that, in Rust, top-level asynchronous functions normally need to be quite conservative with what they own if you want to use them with an actual asynchronous runtime--many of them like to send the thunks between threads in a static thread pool, which limits you to thread safe constructs and owned (or static-borrowed) data. As a result, even if there was no overhead for using an asynchronous function synchronously, you would still in practice have to either sacrifice performance and generality by keeping your data owned and thread-safe (to make things work with thread pools), or embrace all the usual stuff you would do in synchronous code (like borrow things from a previous stack frame) and lose the ability to use your async function at the top level in most existing asynchronous runtimes. So from that standpoint, it's not really up to the caller. This is again not really an issue in garbage collected languages that only allow references to managed, heap-allocated objects.
There are more reasons than that (being able to efficiently call out to blocking C APIs, wanting to use builtin thread locals across function calls without worrying about how the function call is going to be handled, current issues with recursion, etc.). They may be considered artifacts of the implementation or otherwise resolvable--I'm not necessarily saying they aren't--but they are reasons in practice why you want to have synchronous functions available.
So tl;dr I think most people's immediate intuitions about how async/await should just be sugar, or async should be up to the caller (i.e. all functions should be async), don't straightforwardly apply to Rust even if they are valid for most other languages.
I 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.
I'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.
> I 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.
FWIW Python originally used generators to implement async / coroutines, and then went back to add a dedicated syntax (PEP 492), because:
* the confusion of generators and async functions was unhelpful from both technical and documentation standpoints, it's great from a theoretical standpoint (that is async being sugar for generators can be OK, though care must be taken to not cross-pollute the protocols) but it sucks from a practical one
* async yield points make sense in places where generator yield points don't (or are inconvenient) e.g. asynchronous iterators (async for) or asynchronous context managers (async with). Hand-rolling it may not be realistic e.g.
for a in b:
a = await a
is not actually correct, because the async operation is likely the one which defines the end of iteration, so you'd actually have to replace a straightforward iteration with a complete desugaring of the for loop. Plus it means the initialisation of the iterator becomes very weird as you have an iterator yielding an iterator (the outer iterator representing the async operation and the inner representing the actual iterator):
# this yield from drives the coroutine not the iteration
it = yield from iter(b)
while True:
try:
# see above
a = yield from next(it)
except StopIteration: # should handle both synchronous and asynchronous EOI signals, maybe?
break
else:
# process element
versus
async for a in b:
# process element
TBF "blue functions" async remain way more convenient, but they're not always an easy option[0], or one without side-effects (depending on the language core, see gevent for python which does exactly that but does so by monkey patching built-in IO).
The reason we have await in JS is particularly because it is _less_ powerful than generators and you can make more assumptions about it and how one uses it as well as write better tooling.
This is why we have await in Python and C# as well, eventhough we know how to write async/await with coroutines for 10 years now :] Tooling and debugging is super important.
> `async function foo() { const a = await bar();` vs `co(function* foo() { const a = yield bar();` isn't really with adding extra syntax for.
I'd rather pick a standard solution over a library. I never used CO, because I didn't see the benefits of pulling this library in over plain ol' promises. The cognitive overload of generators, yielding, thunks and changing api-s is just too much IMHO, I like simpler things that work just as well.
With the await keyword baked into the language I can simply think of "if anything returns a promise, I can wait for the response in a simple assignment".
I spent quite a lot of time writing Typescript using Fluture and a few other libraries a while ago. Coming from a functional background, combinators + futures actually feels quite natural to me, so I looked at the examples of the "bad" code here and thought I actually quite liked it. But I certainly don't mind the async syntax too much either. Will see how it works out some time!
in my experience, "async" allows you to write the code linearly, but now the code is executed out of order, which is actually much more confusing (for me) to debug and deal with. however, I'm interested to try the rust version to see whether the more explicit types, etc. make this easier to deal with than e.g. `async def` in python, which I find hideously complicated compared to normal event loops.
As 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.
I'm building an async server application (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.
Am 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.
It 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).
It's different in a statically-typed language. The compiler and editor tooling keeps you aware at all times of that interplay. If you forget, you're reminded rather quickly.
As 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!
Can 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?
Hey, 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.
You'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.
[+] [-] divan|6 years ago|reply
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.
[+] [-] naasking|6 years ago|reply
async/await aren't strictly necessary, but to avoid them you need one of:
1. a sufficiently smart compiler with whole program compilation, or
2. to compile two versions of every function, an async variant and a sync variant, or
3. every async wait captures the whole stack, thus wasting a lot of memory.
Async/await is basically a new sort of calling convention, where the program doesn't run in direct style but in continuation-passing style. This permits massive concurrency scaling with little memory overhead, but there other tradeoffs as per above.
Async/await makes perfect sense for Rust which wants to provide zero-overhead abstractions.
[+] [-] Rusky|6 years ago|reply
The choice of `async`-vs-not is not the one you describe. What we label with `async` is a particular compilation style that makes the function interruptible in userspace, in exchange for making synchronous and recursive execution a bit more complicated. This style is important because it gives you event loop-style concurrency without the performance costs of threads (kernel or userspace/green/etc).
However, this compilation style still leaves the choice you describe up to the caller. Most languages seem to reverse the default choice syntactically, but you can still synchronously block on a call to an async function or run it concurrently with something else. It just so happens to be useful primarily for doing the latter, so it gets lumped in with it. (And we get unfortunate misunderstandings like "What Color is Your Function?" that miss this point.)
As an example of a language that doesn't reverse the defaults, look at Kotlin's `suspend fun`s. While they still have `async`-like callee annotations to control the compilation style, a simple function call behaves the same regardless of the callee, and you instead use various `spawn` or CSP-like APIs to get concurrency.
If changing the keyword and making the defaults match threads/CSP isn't enough, perhaps viewing the annotation as part of an effect system would help? The thing being tracked here is "this function can suspend itself mid-execution," and a good effect system even functions be polymorphic over things like this. For example, an effect-polymorphic function passed a closure or interface can automatically take on the effects of its callees, simplifying the program somewhat if you're faced with viral async-ness.
[+] [-] spinningslate|6 years ago|reply
Steve Klabnick provided some useful context on that thread for the Rust decision specifically. More generally though, you've nailed my primary misgiving:
> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.
Erlang/Elixir and Go place the decision with the caller, not the callee. That just seems much more sensible.
An alternative is perhaps to see async/await as dataflow going through the awkward teenager stage. Maybe one day we'll wake up and languages in general will have fully embraced dataflow variables as first order constructs.
[0] https://news.ycombinator.com/item?id=20400991
[+] [-] PJDK|6 years ago|reply
[+] [-] inglor|6 years ago|reply
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.
[+] [-] eridius|6 years ago|reply
If I text you, I expect an asynchronous response. It's ok if you get back to me 3 hours later (well, context-dependent).
If I call you, ask you a question, and then you wait 3 hours before giving me a response, I've been sitting there blocked for 3 hours, unable to get any more work done, because I asked for a synchronous response and got an asynchronous one. Even worse, if you try and give me the response over text, I won't see it because I have the phone pressed to my ear and now I'm blocked forever.
---
The caller and callee have to agree on whether the communication is synchronous or asynchronous. Asking for synchronous and getting asynchronous doesn't work. Asking for asynchronous and getting synchronous kind of works, but that's not truly synchronous, that's just asynchronous with zero delay before getting the response. Or even worse, asking for asynchronous (and giving you a completion handler to fire) and you fire it synchronously before returning control to me, that way lies madness. Don't do that.
[+] [-] brianguertin|6 years ago|reply
EDIT: actually this was wishful thinking, since both functions would block the main thread. The functions need to be modified to "suspend" for this to be truly async.
My ideal language would be something like Kotlin suspend semantics, except ALL functions were implicitly "suspend" functions. If that's even possible...
[+] [-] onebot|6 years ago|reply
This is a exactly how I feel as well and I write Rust code every day.
But no matter, I will suck it up. The language is evolving and I think async/await has, seemingly, been one of the biggest divisive feature the community has faced. I will still use as there are so many other benefits this language brings.
[+] [-] mikedilger|6 years ago|reply
I think of await to mean "When this code is combined with all the other code that needs to be executed asynchronously on the event loop, this code will not be able to progress until this thing we are waiting on finishes. The event loop can therefore use this hint to schedule things and keep the processors busy."
I think I built up a good understanding of how this kind of a system works with futures 0.1 and tokio. It took some time but it all clicked together for me. But as for async/await as language features I'm satisfied to just let it be magic. I don't care to look under the covers at this point. I know it's similar to how futures worked, and I trust the rust team weaved it together well.
[+] [-] atombender|6 years ago|reply
One of the problems with async is that it's viral. Make a function async and your entire call graph has to be made async, unless they handle the future manually without await. Async introduces a new "colour" to the language.
I absolutely agree that asynchronicity should be the provenance of the caller. If you look at typical code in languages with async/await such as JavaScript and C#, awaiting is the common case. So if async causes your code to be littered with awaits anyway, it makes more sense to make awaiting the default (just like it's the default for the rest of the language) and "deferring" the exception. Call it "defer" or something.
(As an aside, I was always disappointed that Go's "go" doesn't return a future, or that indeed there's no "future" support in the standard library. Instead you have to muck about with WaitGroup and ErrGroup and channels, which introduces sequencing even in places that don't need it. Sometimes you just want to spawn N goroutines and then collect all their results in whatever order, and short-circuit then if one of them fails. The inability to forcibly terminate goroutines is another wart here, requiring Context cancellation and careful context management to get right.)
[1] https://news.ycombinator.com/item?id=20405124
[+] [-] pron|6 years ago|reply
[+] [-] bryanlarsen|6 years ago|reply
Also, blocking on a future is trivial (just call poll).
So I would argue that async/await does a better job of leaving it up to the caller to decide: use await if you want async, use poll if you want sync.
[+] [-] pcwalton|6 years ago|reply
CSP as implemented in languages like Go is just threads, but with an idiosyncratic implementation.
[+] [-] lenkite|6 years ago|reply
[+] [-] youeseh|6 years ago|reply
[+] [-] Matthias247|6 years ago|reply
This doesn't work with Rusts implementation, because the functions have slightly different semantics and limitations.
E.g. an async function can't be recursive, since that would lead to a Future type of infinite size.
Async functions also don't necessarily run to completion like normal functions. They can instead also return at any .await point, which are implicit cancellation points. That means if someone just adds an async modifier to a function and some .await calls to methods inside it the result might not be correct anymore, since not all code in the function is guaranteed to be run anymore.
The cancellation mechanism was an explicit choice of the design - it would probably have been possible too to make async functions always run to completion and to thereby avoid the semantic difference.
[+] [-] Tyr42|6 years ago|reply
But "wait until there's eol, than call this callback" or its cousin the future which is "when complete, read here to find the callback(s) to call" which let's you fill in the callback after the call, that's inherently an async call. Same way sending a letter forces you to wait. Sure you could stand by the mailbox until the reply arrives, but that's just converting the async call to synchronous by waiting.
Though I don't really see how futures are that different from a cps, especially if you view them as chaining .then calls, which are exactly continuations. Await just let's you drop some syntax, and unify scopes.
[+] [-] devit|6 years ago|reply
In other words, synchronous procedures are essentially locally CPU-bound, while asynchronous procedures are bound by I/O, network or IO/network/CPU of remote servers.
Async functions are the most efficient way to support asynchronous procedures, while non-blocking non-async functions are the most efficient way to support synchronous procedures.
There are also adapters to turn synchronous function into asynchronous ones, and to turn asynchronous functions into blocking non-async functions, although they should only be used when unavoidable, especiallly the latter.
[+] [-] deepGem|6 years ago|reply
https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9...
[+] [-] sansnomme|6 years ago|reply
[+] [-] nullwasamistake|6 years ago|reply
Languages with fibers figure ou execution suspend points and physical core assignment for you and abstract it all away. So physically they're doing the same thing as async, just without all the cruft.
Rust decided not to go with fibers to avoid having a runtime. I still disagree with this because they already do reference counting, not every worthwhile abstraction is zero cost.
[+] [-] swsieber|6 years ago|reply
I consider Rust to be an antithesis to your remarks because it has realoy good tools to prevent concurrency issues and yet still wants it.
I think on a deeper level it's because there are times where the thread isn't really doing anything and is instead waiting, and that time could be better apent doing something else. That requires sync, either with callbacks, promises or sync/await.
[+] [-] Someone|6 years ago|reply
[+] [-] nnq|6 years ago|reply
Now `async`, you could see it as an implementation detail that you need in order to get much better performance...
[+] [-] rubber_duck|6 years ago|reply
[+] [-] zzzcpan|6 years ago|reply
[1] https://songlh.github.io/paper/go-study.pdf
[+] [-] Jweb_Guru|6 years ago|reply
You can't (in safe Rust) easily replicate what async does for you because it understands how to handle borrows across yield points, which turn into self borrows when you turn the stack into a concrete object in Rust. That's not really an issue in garbage collected languages so it's not quite as much of a necessity to have a specific keyword, but it's still inconvenient in most languages to write everything in CPS or use combinators to acquire thunks.
As for why you would want a function that's not asynchronous... well, various reasons, but one is performance. Creating a thunk and then sending it to a runtime which decides what to do with it (or polling) usually has some overhead compared to linearly calling a function on the stack.
Another reason is that, in Rust, top-level asynchronous functions normally need to be quite conservative with what they own if you want to use them with an actual asynchronous runtime--many of them like to send the thunks between threads in a static thread pool, which limits you to thread safe constructs and owned (or static-borrowed) data. As a result, even if there was no overhead for using an asynchronous function synchronously, you would still in practice have to either sacrifice performance and generality by keeping your data owned and thread-safe (to make things work with thread pools), or embrace all the usual stuff you would do in synchronous code (like borrow things from a previous stack frame) and lose the ability to use your async function at the top level in most existing asynchronous runtimes. So from that standpoint, it's not really up to the caller. This is again not really an issue in garbage collected languages that only allow references to managed, heap-allocated objects.
There are more reasons than that (being able to efficiently call out to blocking C APIs, wanting to use builtin thread locals across function calls without worrying about how the function call is going to be handled, current issues with recursion, etc.). They may be considered artifacts of the implementation or otherwise resolvable--I'm not necessarily saying they aren't--but they are reasons in practice why you want to have synchronous functions available.
So tl;dr I think most people's immediate intuitions about how async/await should just be sugar, or async should be up to the caller (i.e. all functions should be async), don't straightforwardly apply to Rust even if they are valid for most other languages.
[+] [-] jkoudys|6 years ago|reply
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|6 years ago|reply
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.
[+] [-] masklinn|6 years ago|reply
FWIW Python originally used generators to implement async / coroutines, and then went back to add a dedicated syntax (PEP 492), because:
* the confusion of generators and async functions was unhelpful from both technical and documentation standpoints, it's great from a theoretical standpoint (that is async being sugar for generators can be OK, though care must be taken to not cross-pollute the protocols) but it sucks from a practical one
* async yield points make sense in places where generator yield points don't (or are inconvenient) e.g. asynchronous iterators (async for) or asynchronous context managers (async with). Hand-rolling it may not be realistic e.g.
is not actually correct, because the async operation is likely the one which defines the end of iteration, so you'd actually have to replace a straightforward iteration with a complete desugaring of the for loop. Plus it means the initialisation of the iterator becomes very weird as you have an iterator yielding an iterator (the outer iterator representing the async operation and the inner representing the actual iterator): versus TBF "blue functions" async remain way more convenient, but they're not always an easy option[0], or one without side-effects (depending on the language core, see gevent for python which does exactly that but does so by monkey patching built-in IO).[0] assuming async ~ coroutines / userland threads / "green" threads
[+] [-] inglor|6 years ago|reply
This is why we have await in Python and C# as well, eventhough we know how to write async/await with coroutines for 10 years now :] Tooling and debugging is super important.
[+] [-] tiborsaas|6 years ago|reply
I'd rather pick a standard solution over a library. I never used CO, because I didn't see the benefits of pulling this library in over plain ol' promises. The cognitive overload of generators, yielding, thunks and changing api-s is just too much IMHO, I like simpler things that work just as well.
With the await keyword baked into the language I can simply think of "if anything returns a promise, I can wait for the response in a simple assignment".
[+] [-] kolektiv|6 years ago|reply
[+] [-] gobengo|6 years ago|reply
Except in the latter you have to bring your own `co(...)` function. Baking it into the syntax means you don't need that as a library.
[+] [-] jstrong|6 years ago|reply
[+] [-] autarch|6 years ago|reply
[+] [-] hansjorg|6 years ago|reply
[+] [-] iddan|6 years ago|reply
[+] [-] _bxg1|6 years ago|reply
[+] [-] macromaniac|6 years ago|reply
[+] [-] yawaramin|6 years ago|reply
[+] [-] MuffinFlavored|6 years ago|reply
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.
[+] [-] j1elo|6 years ago|reply
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.drdobb...
[1]: http://dunkels.com/adam/pt/
[2]: http://dunkels.com/adam/pt/expansion.html
[+] [-] bfrog|6 years ago|reply
Fantastic work! Look forward to the async/await simplicity
[+] [-] giovannibajo1|6 years ago|reply
[+] [-] unknown|6 years ago|reply
[deleted]
[+] [-] infinity0|6 years ago|reply
[+] [-] dang|6 years ago|reply
https://news.ycombinator.com/newsguidelines.html
[+] [-] steveklabnik|6 years ago|reply