I must be dumb, because every time I dive into async/await, I feel like I reach an epiphany about how it works, and how to use it. Then a week later I read about it again and totally lost all understanding.
What do I gain if I have code like this [0], which has a bunch of `.await?` in sequence?
I know .await != join_thread(), but doesn't execution of the current scope of code halt while it waits for the future we are `.await`-ing to complete?
I know this allows the executor to go poll other futures. But if we haven't explicitly spawned more futures concurrently, via something like task::spawn() or thread::spawn(), then there's nothing else the cpu can possible do in our process?
A good example is say you want to handle 100k TCP sessions concurrently. You probably don't want to launch 100k threads considering the overhead in doing so and constantly switching between them. You also don't want to do things synchronously as you'll constantly be waiting on pauses instead of doing work on the 100k sessions. So you launch 100k instances of it as an async function and they all stay in a single thread (or couple of threads if you want to utilize multiple cores for the work) and instead of constantly waiting on pauses it simply works on the log of queued up events.
Same code flow just it allows you to launch the same thing multiple times without having to wait for the whole thing to finish sequentially or wait on the OS to handle your threads.
So, the power of async await is no greater than the thread pool manager sitting under it.
You are correct. In edge cases where there is only 1 await in the queue for 1 process with 1 thread you gain nothing.
But you're accurately describing an edge case where await has limited value.
Await's true power shows up when you anticipate having multiple in-flight operations that all will, at overlapping points, be waiting on something.
Rather than consume the current thread while waiting, you're telling the run-time, go ahead and resume another task that has reached the end of its await.
This was possible before using various asynchronous design patterns, but all of them were clunky, in that they required boilerplate code to do what the compiler should be able to figure out on its own:
"Hey, runtime. This is an asynchronous call. Go do something useful with this thread and get back to me."
Second, await is MUCH EASIER for future developers to process because it looks exactly like any other method call and makes it easy to reason about the logic flow of the code.
Rather than chasing down async callbacks and other boilerplate concepts to manually handle asynchronous requests, the code reads like its synchronous twin.
int a = await EasyToFollowAsyncIntent();
This makes the code much easier to reason about.
To me those are the 2 biggest gains from async.
1. Less boilerplate code for asynchronous calls.
2. Code remains linearly readable despite being highly asynchronous.
In small examples like this, you don't gain anything. For the sake of the example, we just run one task. But you _could_ run 100 with them. And at each of those `awaits`, they could schedule differently.
Yeah if there are no other futures spawned, then the await is going to cause the app to just sit there until the future completes. It's got nothing better to do.
If there was another future spawned then the await would cause the runtime to sit there until either of the futures completed. The code would attend to the first future that completes intil that hits an await.
Right, that specific instance is essentially a single-threaded* application.
Now imagine that you spawned a few hundred of them with JoinAll. Each would run, multiplexed within a single thread, with execution being passed at the await points.
* anyone know the correct nomenclature for this? Single-coroutine?
Async is a hard concept but what can be revealing is going through the three steps:
1. Get used to callbacks in NodeJS, for example write some code using fs that reads the content of a file, then provide a callback to print that content.
2. Get used to promises in NodeJS, for example turn the code in #1 into a promise by creating a function with that calls the success/reject handler as appropriate on the callback from opening that file. Then use the promise to open the file and use .then(...) to handle the action.
3. Now do it in async. You have the promise, so you just need to await it and you can inline it.
By doing it in the 3 steps I find it is more clear what is really happening with async/await.
> I know .await != join_thread(), but doesn't execution of the current scope of code halt while it waits for the future we are `.await`-ing to complete?
It doesn't, that's the charm of it.
It's best to treat 'await' as syntactic sugar, and to dig in to the underlying concepts.
I realise we're not talking C#/.Net, but that's what I know: in .Net, your function might do slow IO (network activity, say) then process the result to produce an int. Your function will have a return-type of `Task<int>`. Your function will quickly return a non-completed Task object, which will enter a completed state only once the network activity has concluded and processing has occurred to give the final `int` value.
The caller of your function can use the `Task#ContinueWith` method, which enqueues work to occur if/when the Task completes, using the result value from the Task. (We'll ignore exceptions here.)
Internal to your function, the network activity itself will also have taken the form of a standard-library Task, and our function will have made use of its `ContinueWith` method. Things can compose nicely in this way; `Task#ContinueWith` returns another Task.
(We needn't think about the particulars of threads too much here, but some thread clearly eventually marks that Task object as completed, so clearly some thread will be in a good position to 'notice' that it's time to act on that `ContinueWith` now. The continuation generally isn't guaranteed to run on the same thread as where we started. That's generally fine, with some notable exceptions.)
You might think that chain-invoking `ContinueWith` would get tedious, as you'd have to write a new function for each step of the way if we make use of several async operations - each continuation means writing another function to pass to `ContinueWith`, after all. Perhaps it would be more natural to just write one big function and have compiler handle the `ContinueWith` calls.
You'd be right. That's why they invented the `await` keyword, which is essentially just syntactic sugar around .Net's `ContinueWith` method. It also correctly handles exceptions, which would otherwise be error-prone, so it's generally best to avoid writing continuations manually.
There's more machinery at play here of course, but that seems like a good starting point.
Assorted related topics:
* If you use `ContinueWith` on a Task which is already completed, it can just stay on the same thread 'here and now' to run your code
* It's possible to produce already-completed Task objects. Rarely useful, but permitted.
* There's plenty going on with thread-pools and .Net 'contexts'
* The often-overlooked possibility of deadlocking if you aren't careful [0]
* None of this would make sense if we had to keep lots of background threads around to fire our continuations, but we don't [1]
* Going async is not the same thing as parallelising, but Tasks are great for managing parallelism too
* This stuff doesn't improve 'straight-line' performance, but it can greatly improve our scalability by avoiding blocking threads to wait on IO. (That is to say, we can better handle a high rate of requests, but our speed at handling a lone request on a quiet day, will be no better.)
async/await is all about letting you write serial looking code with the smallest memory footprint short of writing hand-coded continuation passing style (CPS) code.
This remind me of the blog post "What Color is Your Function?"[0], they had to create a different library that is the same as the standard library but with async functions.
I thought Rust had other, better ways to create non-blocking code so I don't understand why to use async instead.
> I thought Rust had other, better ways to create non-blocking code so I don't understand why to use async instead.
In fact, Rust does have a great solution for nonblocking code: just use threads! Threads work great, they are very fast on Linux, and solutions such as goroutines are just implementations of threads in userland anyway. (The "what color is your function?" post fails to acknowledge that goroutines are just threads, which is one of my major issues with it.) People tell me that Rust services scale up to thousands of requests per second on Linux by just using 1:1 threads.
Async is there for those who want better performance than what threads/goroutines/etc. can provide. If you don't want to deal with two "colors" of functions, you don't have to! Just use threads.
None of the 5 points in that article about callbacks in 2015 node.js apply to async in Rust. The Rust people spent years agonizing over their version of async and applied a lot of lessons learned from implementations in other languages.
It's trivial to turn async into sync in Rust. You can use ".poll", "executor::block_on", et cetera.
Turning sync into async is harder in any language. Even Go with it's easy threading. That's a good argument to make async the default in libraries in Rust, but since async isn't stable yet, that would have been hard to do 5 years ago.
There are ways to get around this, the way that I have done it in Nim is via a `multisync` macro:
proc readLine(s: Socket | AsyncSocket): Future[string] {.multisync.} =
while true:
let c = await s.recv(1)
case c
of '\n':
return
else:
result.add(c)
This is equivalent to defining two `readLine` procedures, one performing synchronous IO and accepting a `Socket` and another performing asynchronous IO and accepting an `AsyncSocket`. It works very well in practice.
The point of asynchronous programming is to know exactly where concurrency happens in your code. This both eliminates concurrency bugs and gives you predictability for high performance.
The caller of the function knows nothing about what happens within the body of the function. (Is it just doing computation, or is it doing I/O?). The async keyword is how the author of the function makes it explicit that caller should choose when to await the result.
Isn't the alternative WCiYF is proposing to allow the caller to treat any function asynchronously, while having no way to discern whether doing so might be counterproductive?
The right way to handle this stuff is polymorphism. But with Rust lacking higher-kinded types I guess that's not possible. A decent test of whether their "associated type constructors" actually solve the same problems, as sometimes claimed, would be whether you can write this kind of async-polymorphic code with them.
> I thought Rust had other, better ways to create non-blocking code so I don't understand why to use async instead.
'async' exists because Python has that GIL bullshit and so Python programmers had to invent that fifth wheel of 'async programming'.
Programmers in other languages then got jealous because they, too, wanted a complex, unnecessary framework that pollutes the whole runtime and serves to differentiate regular programmers from 'rockstar' programmers.
And so async got fashionable and barely-literate coders now think async is magic performance dust that will automatically make your program run 1000% faster.
TL;DR - it's just fashion, give it five years and we'll be reading posts about how async sucks and that it's stupid legacy tech invented by bonehead dinosaurs.
But it's odd that they do not cite Tokio. I know this isn't an academic paper, but come on have some professional curtesy and discuss the contributions made in prior art.
Apologies if I'm misunderstanding things here, I'm just now getting back into Rust after a couple of years of not using it. Did Tokio really inspire this library that much?
In case anyone else was curious how you create nonblocking file I/O, it appears to use threads.
I am curious if the number of threads is unbounded, or if they have a bounded set but accept deadlocks, or if there is a third option other than those two that I am unaware of.
Are those really the only options? I'm trying to wrap my head around how using a fixed size thread pool for I/O automatically implies deadlocks but I just can't. Unless the threads block on completion until their results are consumed instead of just notifying and then taking the next task..
I can definitely imagine blocking happening while waiting for a worker to be available, though. Did you mean simply blocking instead of deadlock?
Rust is very ambitious and unusually successful at reaching its ambitions. It's efficient like C/C++, but safer. It's modern like Go, but more expressive and open to metaprogramming. It's often as readable as a scripting language, but doesn't depend on garbage collection. It's a young rising star originating from a great company.
This is bunk. Simply run this search[1] and behold the stream of Rust related submissions that get no play at all; zero comments and no more than one or two up votes. These instances of highly ranked rust stories are actually the exception; no more than one or two a week typically. The rest of the Rust stuff is seen by almost no one.
Rust is the first language basically ever that has a serious chance at displacing C and C++. Combined with a type system that eliminates multiple error classes, and some functional aspects, it's a very interesting language.
1) It's built in large part by Mozilla, which enjoys special love as an OSS company.
2) Rust provides tangible benefits (memory/concurrency safety) over existing languages in an important niche (performance-sensitive programs).
3) The community is nice and talented so it's fun to see what they're up to.
It does the scheduling for you. That's why all Futures put onto task through `async_std::task` must be `Send`. That's Rust parlance for "can be safely migrated between threads".
It's not Go, but we know what people like about Go. <3
Has preemptive scheduling landed in Go ? Because last time I worked with it (Go 1.10) it was still cooperative and you had to worry about it otherwise you could get bitten badly.
Great library, well done. In case anyone was wondering this is not a [no_std] crate even though it can be used as a replacement for std library calls. I guess it (obviously) can't be since it interfaces with the operating system so much.
It exports stdlib types (like io::Error) where appropriate so that libraries working with these can stay compatible, so `no_std` is not really an option.
The underlying library (async-task) is essentially core + liballoc, just no one made the effort to spell that out, yet.
I really wish we could work out a better way to make no_std easier. I know why io::Error requires std for example, but it makes things difficult.
It would be nice if you could provide your own sys crate so you could even use some of std on an embedded device. If you had say an RTC you could make time related calls work, maybe you'd wire networking to smoltcp etc. Currently you could do that - maybe - but you'd have to modify the Rust standard library.
[+] [-] 2bitencryption|6 years ago|reply
What do I gain if I have code like this [0], which has a bunch of `.await?` in sequence?
I know .await != join_thread(), but doesn't execution of the current scope of code halt while it waits for the future we are `.await`-ing to complete?
I know this allows the executor to go poll other futures. But if we haven't explicitly spawned more futures concurrently, via something like task::spawn() or thread::spawn(), then there's nothing else the cpu can possible do in our process?
[0] https://github.com/async-rs/async-std/blob/master/examples/t...
[+] [-] zamadatix|6 years ago|reply
Same code flow just it allows you to launch the same thing multiple times without having to wait for the whole thing to finish sequentially or wait on the OS to handle your threads.
[+] [-] cheez|6 years ago|reply
Here is synchronous code:
Here is synchronous code, that tries to be asynchronous: Once server.getStuff returns, the callback passed to it is called with the result.Here is the same code with async/await:
Internally, the compiler rewrites it to (roughly) the second form. That's called a continuation.That's pretty much it.
A more involved example.
Synchronous code:
Synchronous code that tries to be asynchronous: A lot of JS code used to look like this hideous monstrosity.Async/await version:
Remember again, that it is basically transformed by the compiler into the second form.[+] [-] ijidak|6 years ago|reply
You are correct. In edge cases where there is only 1 await in the queue for 1 process with 1 thread you gain nothing.
But you're accurately describing an edge case where await has limited value.
Await's true power shows up when you anticipate having multiple in-flight operations that all will, at overlapping points, be waiting on something.
Rather than consume the current thread while waiting, you're telling the run-time, go ahead and resume another task that has reached the end of its await.
This was possible before using various asynchronous design patterns, but all of them were clunky, in that they required boilerplate code to do what the compiler should be able to figure out on its own:
"Hey, runtime. This is an asynchronous call. Go do something useful with this thread and get back to me."
Second, await is MUCH EASIER for future developers to process because it looks exactly like any other method call and makes it easy to reason about the logic flow of the code.
Rather than chasing down async callbacks and other boilerplate concepts to manually handle asynchronous requests, the code reads like its synchronous twin.
int a = await EasyToFollowAsyncIntent();
This makes the code much easier to reason about.
To me those are the 2 biggest gains from async.
1. Less boilerplate code for asynchronous calls.
2. Code remains linearly readable despite being highly asynchronous.
[+] [-] Argorak|6 years ago|reply
For a more complex networked application, we have the tutorial here: https://github.com/async-rs/a-chat
[+] [-] weavie|6 years ago|reply
If there was another future spawned then the await would cause the runtime to sit there until either of the futures completed. The code would attend to the first future that completes intil that hits an await.
[+] [-] esotericn|6 years ago|reply
Now imagine that you spawned a few hundred of them with JoinAll. Each would run, multiplexed within a single thread, with execution being passed at the await points.
* anyone know the correct nomenclature for this? Single-coroutine?
[+] [-] MusharibSajid|6 years ago|reply
1. Get used to callbacks in NodeJS, for example write some code using fs that reads the content of a file, then provide a callback to print that content.
2. Get used to promises in NodeJS, for example turn the code in #1 into a promise by creating a function with that calls the success/reject handler as appropriate on the callback from opening that file. Then use the promise to open the file and use .then(...) to handle the action.
3. Now do it in async. You have the promise, so you just need to await it and you can inline it.
By doing it in the 3 steps I find it is more clear what is really happening with async/await.
[+] [-] nurettin|6 years ago|reply
If you have an async method that can wait for input on both devices, you can await the results of both of them, and they won't block eachother.
[+] [-] MaxBarraclough|6 years ago|reply
Nope, async really isn't trivial.
> I know .await != join_thread(), but doesn't execution of the current scope of code halt while it waits for the future we are `.await`-ing to complete?
It doesn't, that's the charm of it.
It's best to treat 'await' as syntactic sugar, and to dig in to the underlying concepts.
I realise we're not talking C#/.Net, but that's what I know: in .Net, your function might do slow IO (network activity, say) then process the result to produce an int. Your function will have a return-type of `Task<int>`. Your function will quickly return a non-completed Task object, which will enter a completed state only once the network activity has concluded and processing has occurred to give the final `int` value.
The caller of your function can use the `Task#ContinueWith` method, which enqueues work to occur if/when the Task completes, using the result value from the Task. (We'll ignore exceptions here.)
Internal to your function, the network activity itself will also have taken the form of a standard-library Task, and our function will have made use of its `ContinueWith` method. Things can compose nicely in this way; `Task#ContinueWith` returns another Task.
(We needn't think about the particulars of threads too much here, but some thread clearly eventually marks that Task object as completed, so clearly some thread will be in a good position to 'notice' that it's time to act on that `ContinueWith` now. The continuation generally isn't guaranteed to run on the same thread as where we started. That's generally fine, with some notable exceptions.)
You might think that chain-invoking `ContinueWith` would get tedious, as you'd have to write a new function for each step of the way if we make use of several async operations - each continuation means writing another function to pass to `ContinueWith`, after all. Perhaps it would be more natural to just write one big function and have compiler handle the `ContinueWith` calls.
You'd be right. That's why they invented the `await` keyword, which is essentially just syntactic sugar around .Net's `ContinueWith` method. It also correctly handles exceptions, which would otherwise be error-prone, so it's generally best to avoid writing continuations manually.
There's more machinery at play here of course, but that seems like a good starting point.
Assorted related topics:
* If you use `ContinueWith` on a Task which is already completed, it can just stay on the same thread 'here and now' to run your code
* It's possible to produce already-completed Task objects. Rarely useful, but permitted.
* There's plenty going on with thread-pools and .Net 'contexts'
* The often-overlooked possibility of deadlocking if you aren't careful [0]
* None of this would make sense if we had to keep lots of background threads around to fire our continuations, but we don't [1]
* Going async is not the same thing as parallelising, but Tasks are great for managing parallelism too
* This stuff doesn't improve 'straight-line' performance, but it can greatly improve our scalability by avoiding blocking threads to wait on IO. (That is to say, we can better handle a high rate of requests, but our speed at handling a lone request on a quiet day, will be no better.)
I found this overview to be fairly digestible [2]
[0] https://blog.stephencleary.com/2012/07/dont-block-on-async-c...
[1] https://blog.stephencleary.com/2013/11/there-is-no-thread.ht...
[2] https://stackoverflow.com/a/39796872/
See also:
https://docs.microsoft.com/en-us/dotnet/standard/parallel-pr...
https://docs.microsoft.com/en-us/dotnet/api/system.threading...
[+] [-] cryptonector|6 years ago|reply
[+] [-] justicezyx|6 years ago|reply
Async can be useful when more control over the details of execution is needed.
[+] [-] Un1corn|6 years ago|reply
I thought Rust had other, better ways to create non-blocking code so I don't understand why to use async instead.
[0] https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
[+] [-] pcwalton|6 years ago|reply
In fact, Rust does have a great solution for nonblocking code: just use threads! Threads work great, they are very fast on Linux, and solutions such as goroutines are just implementations of threads in userland anyway. (The "what color is your function?" post fails to acknowledge that goroutines are just threads, which is one of my major issues with it.) People tell me that Rust services scale up to thousands of requests per second on Linux by just using 1:1 threads.
Async is there for those who want better performance than what threads/goroutines/etc. can provide. If you don't want to deal with two "colors" of functions, you don't have to! Just use threads.
[+] [-] Jayschwa|6 years ago|reply
[0]: https://ziglang.org
[+] [-] bryanlarsen|6 years ago|reply
https://news.ycombinator.com/item?id=20676641
It's trivial to turn async into sync in Rust. You can use ".poll", "executor::block_on", et cetera.
Turning sync into async is harder in any language. Even Go with it's easy threading. That's a good argument to make async the default in libraries in Rust, but since async isn't stable yet, that would have been hard to do 5 years ago.
[+] [-] dom96|6 years ago|reply
[+] [-] epage|6 years ago|reply
The main challenges I see are around usability within the language design on how best to propagate and compose them.
[0] https://www.reddit.com/r/rust/comments/cjcwmu/is_there_inter...
[+] [-] zzzcpan|6 years ago|reply
[+] [-] pohl|6 years ago|reply
Isn't the alternative WCiYF is proposing to allow the caller to treat any function asynchronously, while having no way to discern whether doing so might be counterproductive?
[+] [-] lmm|6 years ago|reply
[+] [-] otabdeveloper2|6 years ago|reply
'async' exists because Python has that GIL bullshit and so Python programmers had to invent that fifth wheel of 'async programming'.
Programmers in other languages then got jealous because they, too, wanted a complex, unnecessary framework that pollutes the whole runtime and serves to differentiate regular programmers from 'rockstar' programmers.
And so async got fashionable and barely-literate coders now think async is magic performance dust that will automatically make your program run 1000% faster.
TL;DR - it's just fashion, give it five years and we'll be reading posts about how async sucks and that it's stupid legacy tech invented by bonehead dinosaurs.
[+] [-] lachlan-sneff|6 years ago|reply
[+] [-] limsup|6 years ago|reply
But it's odd that they do not cite Tokio. I know this isn't an academic paper, but come on have some professional curtesy and discuss the contributions made in prior art.
[+] [-] ShinTakuya|6 years ago|reply
[+] [-] evmar|6 years ago|reply
I am curious if the number of threads is unbounded, or if they have a bounded set but accept deadlocks, or if there is a third option other than those two that I am unaware of.
[+] [-] Argorak|6 years ago|reply
https://github.com/async-rs/async-std/pulls?utf8=%E2%9C%93&q...
[+] [-] slovenlyrobot|6 years ago|reply
[+] [-] rmgraham|6 years ago|reply
I can definitely imagine blocking happening while waiting for a worker to be available, though. Did you mean simply blocking instead of deadlock?
[+] [-] layoutIfNeeded|6 years ago|reply
[+] [-] jedisct1|6 years ago|reply
[+] [-] Argorak|6 years ago|reply
[+] [-] mintplant|6 years ago|reply
[0] https://github.com/tokio-rs/tokio
[+] [-] rammy1234|6 years ago|reply
[+] [-] hathawsh|6 years ago|reply
[+] [-] topspin|6 years ago|reply
This is bunk. Simply run this search[1] and behold the stream of Rust related submissions that get no play at all; zero comments and no more than one or two up votes. These instances of highly ranked rust stories are actually the exception; no more than one or two a week typically. The rest of the Rust stuff is seen by almost no one.
[1] https://hn.algolia.com/?query=rust&sort=byDate&prefix&page=1...
[+] [-] jnbiche|6 years ago|reply
[+] [-] pradn|6 years ago|reply
[+] [-] monocasa|6 years ago|reply
[+] [-] brians|6 years ago|reply
Features like Haskell—destructuring bind, useful type system.
Performance like C, including no GC.
[+] [-] ggregoire|6 years ago|reply
Makes it a good option when reliability and performance matter (think web browser, database or anything at the OS level).
[+] [-] jedisct1|6 years ago|reply
The thing I like in Go is that I don’t have to worry about that, it’s all automatic.
[+] [-] Argorak|6 years ago|reply
It's not Go, but we know what people like about Go. <3
[+] [-] pythonist|6 years ago|reply
Go is such a joy to work with.
[+] [-] littlestymaar|6 years ago|reply
[+] [-] pcwalton|6 years ago|reply
[+] [-] davidhyde|6 years ago|reply
[+] [-] Argorak|6 years ago|reply
It exports stdlib types (like io::Error) where appropriate so that libraries working with these can stay compatible, so `no_std` is not really an option.
The underlying library (async-task) is essentially core + liballoc, just no one made the effort to spell that out, yet.
[+] [-] bpye|6 years ago|reply
It would be nice if you could provide your own sys crate so you could even use some of std on an embedded device. If you had say an RTC you could make time related calls work, maybe you'd wire networking to smoltcp etc. Currently you could do that - maybe - but you'd have to modify the Rust standard library.
[+] [-] bfrog|6 years ago|reply