6 ms·
Am I the only one struggling with async/await in _any_ language? I did my fair share of tornado/twisted/zeromq ioloop programming, and even though I completely
by Galanwe 6y ago
Am I the only one struggling with async/await in _any_ language?
I did my fair share of tornado/twisted/zeromq ioloop programming, and even though I completely agree that it gets messy real quick, I always "got it".
With async/await though, I always struggle and need a really long time to get a clear picture of what is happening.
Maybe my mental model of how it works is not optimal so I would love for people to describe how they "read" async/await code.
I typically see await very much like I would read a "yield", except that instead of yielding a value, it just yields a promise to the ioloop that will "next()" once it resolves.
Still, it seems every language is adopting it, so I guess à majority of people find it simpler to reason with, I just don't :(
Note: the same applies for async/await in JS. I can easily get my head around the "then" style, but struggle with async/await. A bit less than in python though.
- theelous3 6y agoTry curio or trio in python :) Trio especially with its requirement of an owner for each task makes the thinking of it all entirely sequential and clear. In curio it's optional. See the rationale here: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ https://vorpus.org/blog/notes-on-structured-concurrency-or-g... On top of that, I must recommend dabeaz's concurrency from the ground up talk, where he explains it all in primitive terms and lives codes a wee co-operative yielding thing on the fly: https://youtu.be/MCs5OvhV9S4 https://youtu.be/MCs5OvhV9S4
- crubier 6y agoCurio is really good and underrated
- theelous3 6y agoYep, absolutely exceptional library. Given its existence, I honestly don't understand why asyncio is in use by anyone. It should be removed from stdlib as a mistake and efforts around the curio / trio ecosys should be redoubled and brought to the mainstream. It's clear now, all this time after asyncawait was brought in, that the thinking which lead to asyncio was erroneous. I'm the author of a sort of popular curio/trio lib. I used to maintain a compatibility layer lib that allowed code to be written to use either or, just for my project. That torch has been taken up by anyio, allowing anyone who's writing something for either curio or trio to kill two birds with one stone and contrib to both sides at once. Is great.
- formerly_proven 6y agoTry to grasp coroutines outside the async/await framework. Alternatively, try to understand corutines within the framework as state machines, like so: async def foo(): conn = await DB.connect() results = await conn.query("SELECT bar, baz;") restresult = await REST.get("somewhere.else.prod.example.net/v1/BAR", id=results.id) return restresult.xyz -> class foo(): future = None conn = None results = None restresult = None def call(self): self.future = Future() future = DB.connect() future.add_done_callback(self._db_conn) return self.future def _db_conn(self, fut): try: self.conn = fut.get_result() except: self.future.set_exception() return future = self.conn.query("SELECT bar, baz;") future.add_done_callback(self._results) def _results(self, fut): try: self.results = fut.get_result() except: self.future.set_exception() return future = REST.get("somewhere.else.prod.example.net/v1/BAR", id=self.results.id) future.add_done_callback(self._restresult) def _restresult(self, fut): try: self.restresult = fut.get_result() except: self.future.set_exception() return self.future.set_result(self.restresult.xyz)
- jnwatson 6y agoI think this bottom-up approach is the best way to understand. Using libevent or libev for a while is a great way to understand the power of async. Async re-merges functions that have been shattered due to having to block.
- formerly_proven 6y agoThere are some projects which highlight what the language is doing versus what a library is implementing on top of that (e.g. asynker). I found that a great help in figuring out "what" async "is".
- croh 6y agoTwisted supports both styles, if I am not wrong. Top looks like inline callbacks api while bottom looks like normal defer api.
- crubier 6y agoPersonally I find it’s the other way around. Promises are so easy to understand. They are literally that: the promise of a result that you can await later on. Maybe another helpful way to see it is that each sync function is one atomic block that is executed in one go. On the other hand, see each « await » in an async function as a split of the function into atomic blocks that can be executed separately, allowing to do other things while waiting between each block. This abstraction catches up because it works really well. It is very composable as well. For example adding a queue instead of naively processing a lot of promises at once is 2 lines of code with this https://www.npmjs.com//promise-queue https://www.npmjs.com//promise-queue Now I am waiting for async generators to catch up as well, to replace streams... I recently worked with this package for example and it was so much better than plain streams https://www.npmjs.com/package/streaming-iterables https://www.npmjs.com/package/streaming-iterables
- pansa2 6y ago> Promises are so easy to understand. I still don't understand promises in JavaScript. I got my head around the convoluted constructor (a function whose argument is a function whose arguments are two functions), but I can't make sense of chaining and the "promise resolution procedure" [0]. However I've looked into async-await in Python and it seems straightforward. Doesn't `await foo()` just mean "suspend execution here and don't run any more code in this function until foo is done"? [0] https://promisesaplus.com/#the-promise-resolution-procedure https://promisesaplus.com/#the-promise-resolution-procedure
- vorticalbox 6y agoYes and that's what a promise is in a javascript. When you call await on a function it gets put into the event loop and when it's read the processing in the function will resume.
- tehlike 6y agoIn recent versions of javascript, you can use almost the same async/await concept. It's the same thing, it's just that original syntax of promise is a bit too verbose.
- ZephyrBlu 6y agoI'm not 100% on what's going on under the hood, but the way I think of async/await is literally as the name suggests. The code executes asynchronously, and if there's a specific operation I want to wait for before continuing execution, I signal that I want to synchronously execute this block of code using await. I understand this is not technically how async/await works, however I believe it's an easy way to read and understand how async code will execute. For example: async def func(): # do some stuff asynchronously # wait to get values before continuing execution values = await get_values() # do more stuff with values asynchronously
- dnautics 6y agoThe thing is that in other languages (F#, c++, elixir) async is a directive to run a (normal or normal-looking) function asynchronously, with await being the directive for the calling function to catch the asyncd function's future.
- deleted 6y ago[deleted]
- Stratoscope 6y agoThat looks like a good way to understand it, but I think you have "synchronous" and "asynchronous" reversed. When you're running ordinary Python code, it is synchronous - it runs line by line and has control of the CPU. When you await on another function, that is the asynchronous part - your code is suspended until that other code completes. And then after the await is satisfied, your function starts to run synchronously again.
- jonahbenton 6y agoYou are not. Strong opinion- async/await as function-level/programmer-exposed abstractions is broken. From a mental model perspective, I would recommend Communicating Sequential Processes - https://en.m.wikipedia.org/wiki/Communicating_sequential_processes https://en.m.wikipedia.org/wiki/Communicating_sequential_pro... - and see Golang's channels and goroutines as a pretty good surface. Clojure also has this surface in the core.async library. CSP is about asynchrony in the large- decoupled concurrent activities, using data to coordinate, that more closely reflect mental models of how the real world operates. Asynchrony in the small, at the line-of-code/function-spec level, is terrible. Its exposure completely defeats Python's very concrete, literal pseudocode-as-code origin story. And its use prevents the machine from doing what it does well-scheduling and efficient execution. You are not the only one. It is a shame that these tools have been added to the toolbox, and the conceptual spaghetti they lead to today is tomorrow's technical debt. Cheers.
- monadic2 6y agoCan you illustrate how/why the abstraction is broken?
- jonahbenton 6y agoWell, as with all conceptual discussions, clarity, receptivity and sufficiency are a function both of speaker and of audience. The variations in both produce a multiplicity of arguments, and googling for why async/await suck will lead to rich troves to explore. I can't reproduce the spectrum in anywhere near finite time. But I'll go with the two most cogent for me. The first and most important is that Python made its bones with an enforced single thread execution runtime- with the GIL- that dramatically simplified the mental machinery a programmer needed to do productive things with computers. Extremely successful approach. Async/await, as keywords- core parts of the language- are a complete conceptual departure. Like Python was a very safe multipurpose Swiss Army knife, that now as part of the core has a powersaw. For people who need the powersaw capability, with the ergonomics of a simple blade, Python has always had libraries and module extensibility, and the brilliance of the dunder methods that give access to the machinery for experts who need it. Clear distinction between 95% and 5% use case tooling. Second, as concurrency-enabling abstractions, async/await are poor because they are an opinion that what is hard about concurrency is code. This is a bad opinion. It is data that is hard. When you are dealing with things happening in parallel, trying to understand them, you realize that the understanding part is only hard when there are dependencies between the parallel things. In computing terms, those dependencies are expressed with data relationships. One process waits for another only because it needs some data from it, or it needs it to be ready to receive some data. Things that don't share data, in one form or another, do not care about each other. Queues- a data structure- help model data dependencies explicitly. Async/await only model them implicitly, and poorly, at that. For every await, there is a use case for a queue that much more clearly and composably expresses the true dependency between the potentially concurrent operations. Async/await- as conceptual flags for code blocks- are helpful optimizations for experts, where the operational ergonomics of interacting concurrent processes are well known enough. And for experts they help unify two different concurrency domains- processor-based being one, io-based being the other. This is why you see examples of use of these in state machines and ioloops, both expert level optimization constructs. But even there, they are attributes of a code block, something that should be reachable through some dunder-token, as are other expert level machinery/magic. To add them as top level standalone keywords in a language lauded for being executable pseudocode is an astonishing misunderstanding. Hope that is helpful. Again, CSP is a much more approachable mental model. A Golang-like channel construct would have been a much better addition to the language in place of await. And exposing a not-directly-executable dunder flag much better than async. Cheers.
- mystickphoenix 6y agoI'm unable to find the particular article that made async click for me with Python but I'll try to paraphrase the conceptual idea while trying to ignore language differences as much as I can: Async function calls work like "normal" until they hit a yield statement. When they do, they yell back up the callstack "hey, I'm going to be waiting for a while, let something else happen while I am". When that happens, one of two things can happen. 1. Either there's nothing else to do, in which case we wait until something that has yielded to no longer be waiting. 2. Something else is done waiting and it's waiting for its turn. In which case, it's allowed to continue at this point until it either returns or yields again. In this sense, instead of yielding a value, the function is yielding the stack to something else that can use it while it's waiting. Async is great for when you're waiting on external processes to complete (database calls, API calls, etc). It's terrible for any CPU heavy tasks because, at least in the Python world, it can still only use a single CPU. In this way, again, specific to Python, it's use-case is much closer to multithreading than multiprocessing. Another way to think about the machinery behind the scenes is it's a queue of callstacks. If an async call yields the primary stack, its callstack is thrown onto the queue. The queue of callstacks is then processed in order. Hope that helps! Took me a long while to wrap my head around it and I still struggle reading it some of the time.
- deleted 6y ago[deleted]