6 ms·
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 abo
by 2bitencryption 7y ago
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?
[0] https://github.com/async-rs/async-std/blob/master/examples/tcp-client.rs https://github.com/async-rs/async-std/blob/master/examples/t...
- Argorak 7y agoIn 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. For a more complex networked application, we have the tutorial here: https://github.com/async-rs/a-chat https://github.com/async-rs/a-chat
- pvorb 7y agoWouldn't it make more sense to show an example that actually takes advantage of async/await? I don't get why they are using examples that need a disclaimer like you can run 100 jobs for this to make sense. So it should include that in the example (and it should probably do something that makes sense if it's run a hundred times).
- esotericn 7y agoThe example is intended for you to be able to implement it, not as a showcase. I think the expectation with Rust async-await at the moment is likely that people are familiar with async syntax from other languages e.g. Python - it's not even in beta yet, you need to be running nightly to get the syntax.
- weavie 7y agoYeah 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.
- marshray 7y agoAnd where it all comes together is when async lets us write concurrent functions that compose together well in a way that functions that can block on IO and lock acquision do not.
- ori_b 7y agoHow so? The same API is trivial to implement using threads and futures. A future, after all, is just a one shot channel or rendezvous point. Async is just a way to get cooperative threads compiled to a static state machine, trading lower concurrent utilization and throughput for less context switch overhead and lower latency.
- esotericn 7y agoRight, 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?
- fzzzy 7y agoCooperative threads are still threads, they just aren't preemptive.
- esotericn 7y agoI suppose you're right. The example is both single-threaded at the OS level and single-threaded at the program level.
- staticassertion 7y agoSerial?
- zamadatix 7y agoA 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.
- Thaxll 7y agoThis is missing a crutial explanation that the underlying OS API are asynchronous.
- nullwasamistake 7y agoYeah for sure. In Java/C# I see people do this all the damn time. Use async method for REST endpoints then make a blocking DB call. Or even worse, make a non-async REST call to another service from inside an async handler. As soon as you do that, your code isn't async anymore. And if you're using a framework like Vert.X or node that only runs one thread per core you're in big trouble. The most reasonable answer I've seen to all this is Java's Project Loom. An attempt to make fibers transparently act like threads, so you can use regular threaded libraries as async code. Rust is going to have the same problem Java does with async. A lot of code was written way before async was available, and it not always obvious whether something blocks.
- cgh 7y agoA message broker works here when you want async behaviour but you are integrating with sync code. To use your REST example, you receive the call, send a message to DoSomething and then immediately return http 202, perhaps with some id the ui can poll on (if required). Meanwhile, the DoSomething message queue is serviced by a few threads.
- MaxBarraclough 7y ago> I must be dumb 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-code.html https://blog.stephencleary.com/2012/07/dont-block-on-async-c... [1] https://blog.stephencleary.com/2013/11/there-is-no-thread.html https://blog.stephencleary.com/2013/11/there-is-no-thread.ht... [2] https://stackoverflow.com/a/39796872/ https://stackoverflow.com/a/39796872/ See also: https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/chaining-tasks-by-using-continuation-tasks https://docs.microsoft.com/en-us/dotnet/standard/parallel-pr... https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.continuewith?view=netframework-4.8#System_Threading_Tasks_Task_ContinueWith_System_Action_System_Threading_Tasks_Task_System_Object__System_Object_ https://docs.microsoft.com/en-us/dotnet/api/system.threading...
- cheez 7y agoasync/await are coroutines and continuations (bear with me). Here is synchronous code: result = server.getStuff() print(result) Here is synchronous code, that tries to be asynchronous: server.getStuff(lambda result: print(result)) Once server.getStuff returns, the callback passed to it is called with the result. Here is the same code with async/await: result = await server.getStuff() print(result) 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: result = server.getStuff() second = server.getMoreStuff(result+1) print(result) Synchronous code that tries to be asynchronous: server.getStuff( lambda result: server.getMoreStuff( result+1, lambda result2: print(result2) )) A lot of JS code used to look like this hideous monstrosity. Async/await version: result = await server.getStuff() second = await server.getMoreStuff(result+1) print(result) Remember again, that it is basically transformed by the compiler into the second form.
- arcticbull 7y agoThis is an incredibly helpful explanation. I went from not really knowing what all this mumbo jumbo was about to a useful mental model. Cheers!
- 2bitencryption 7y agoThanks. Helpful. My question is, in this example: result = await server.getStuff() second = await server.getMoreStuff(result+1) print(result) `await getStuff()` MUST terminate before `await getMoreStuff() ` begins. So this chunk alone is analagous to synchronous code, unless we're in the middle of a spawned task, and there are other spawned tasks in the executor that can be picked up.
- csande17 7y agoYup, your understanding is correct. That code behaves equivalently to the synchronous version, and the only benefit is that the thread can run other tasks while it's waiting for the getStuff() and getMoreStuff() to come back. Async/await is really popular in the JavaScript community because in web apps, you usually only have a single thread of execution which you share with the browser UI code. So if your code made a network request synchronously, the user might not be able to scroll or click links or anything until it finished.
- ijidak 7y agoSo, 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.
- nurettin 7y agoSay you want to listen to a socket and also receive input from the keyboard at the same time. 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.
- justicezyx 7y agoUser space threads and corouting is pretty much the right abstraction for application code. Async can be useful when more control over the details of execution is needed.
- twblalock 7y agoAsync/await and futures/promises (and before that, stuff like Java's Executor abstraction) are being added to a lot of languages because it is very difficult for even experienced developers to manage threads in a bug-free way. I've seen a lot of people try to manage complex programs by working with threads directly. Whatever they come up with is very unlikely to be as correct and reliable as the abstractions provided by the language. Even when they get it right, programs written with those techniques are difficult to modify without introducing new bugs. Manual management of threads is becoming like manual management of memory -- it is discouraged by newer language features and you should only do if you really need to.
- MusharibSajid 7y agoAsync 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.
- cryptonector 7y agoasync/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.