So what this does is introduce a (configurable) buffer between chained async generators. So this does not introduce any kind of multi processor parallelism, just makes an async generator easily be able to wait on multiple iterations in parallel. So basically the need for queues is abstracted away, looks quite nice if your use case requires something like this
I was slightly torn on the usage of the word "parallelise", in that yes, CPU-wise, no tasks will progress in parallel.
However, the point of the library is to allow certain parts of a pipeline to progress at the same time in the real-world. In the example, they are calls to asyncio.sleep, but in real cases, they could be HTTP requests. As in, the bytes of the request/response will be going across the wire at the same time, so the total wall-clock time of (certain) pipelines will go down.
I am familiar with all of the cheats, delaying evaluation, kicking the can onto the network and pretending that everything isn't just being fed into a mutex on the other side.
And for writing to disk, async is basically the only way to "parallelize" such operations
I just think its inappropriate because, if I was looking for a parallelization library like multiprocessing, and I found this, I might waste 15 minutes or so before I figure out that this isn't parallel, it's just async.
Async is okay, but it does nothing to improve performance, like what parallelization does. Async provides good UX, parallelization provides good performance. Conflating the two just muddys the water for the people who like to do high-performance-computing.
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.
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:
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.
There 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".
Personally 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
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
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"?
Yes 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.
In 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.
Async await in python and in JS work exactly the same way (except that you have to explicitly launch an event loop in python). I have developed a library that makes heavy uses of promises and async generators, and the code for the python and JS version are extremely similar. Like similar enough that an automatic translation could be done.
And yes, forget about .then and other old school stuff like that in JS. Just use async await to deal with promises.
I'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
The 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.
That 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.
You 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_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.
Well, 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.
I'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.
Yeah, this actually looks pretty cool, it's a nice way to abstract a common pattern which is otherwise quite tedious, namely, that of spawning a bunch of tasks and have them feed each other work via asyncio queues.
Have you thought about making it work as a decorator?
Indirectly: the initial version was a single function, so could have been used in a decorator easily enough.
The issue is what happens on exceptions to avoid tasks hanging around forever. Exceptions propagate “forward” in the pipeline fairly automatically. However, getting the ones “behind” the exception to notice: that was tricky. The best I came up with was the bit of state wrapped up in buffered_pipeline that notices the exceptions and cancels the tasks.
I'm not saying the OP code isn't useful, but I do think that readers may be unaware of existing documented features and jumping into a library to solve their perceived problems is short changing the existing mechanisms.
I've gotten along ok with create_task and gather. I'll need to spend some time with this implementation to see if it fits any of my existing asyncio coding patterns.
Yeah, I was thinking along those lines. A more readable version of this library that I sketched out might be something like --
An eagerly executing coroutine to consume the original generator and put it in a queue, and a new simple generator that yields values from the queue till it's told to stop.
I'm not sure if this matches the behavior of the original library for task cancellation though.
def eager_gen(gen):
end_marker = object()
queue = asyncio.Queue()
async def consumer():
try:
async for val in gen:
await queue.put(val)
except Exception as e:
await queue.put((end_marker, e))
else:
await queue.put((end_marker, None))
async def new_gen():
while True:
item = await queue.get()
if isinstance(item, tuple) and item[0] == end_marker:
break
yield item
if item[1]:
raise item[1] from None
_task = asyncio.create_task(consumer())
return new_gen()
Concurrency/Parallelism is the number one reason I move a project from Python to any other language. It's one the few areas where Python can't provide a gracious frontend or an acceptable runtime, but it is very impressive how much the community is willing to do to mitigate the challenges.
Process/Thread pools help to mitigate the brain damage in Python significantly, but I hear you. Python's concurrency story is the main reason I started looking at other languages for a better concurrency story and it's why I picked up Clojure as a second language.
How does this compare to something like Rx? http://reactivex.io/languages.html is it not applicable because generators are pull based an observables are push based? Could the generator be wrapped in an observable (as I commonly do with promises which are eager) to compose them that way?
I had a lot of trouble writing and maintaining an asyncio Python codebase. It's still hard to find library support, and there are "function coloring"[1] issues that tend to make asyncio usage spread.
So I started using multiprocessing and multithreading available via concurrent.futures. I built a wrapper package called `ori`[2] that lets you build chains of threadpools and processpools without having to enter the Python asyncio kingdom.
We have something very similar to this that we wrote at my work.
Ours is based off Twisted, which works pretty similarly to the new async stuff in python3.
The thing ours does, that I can't tell if this one supports or not, is you pass it a list of functions you want it to call (in python3 you would just pass it coroutines I suppose, but in Twisted an async function gets scheduled automatically, so you need to pass it in an uncalled state), and then you specify the "width" of the pipeline, and it runs through your list of calls, ensuring that "width" number of calls are in flight at a time.
It's a super useful little utility to have around.
43 comments
[ 3.1 ms ] story [ 66.7 ms ] thread"Parallelized async"
I was slightly torn on the usage of the word "parallelise", in that yes, CPU-wise, no tasks will progress in parallel.
However, the point of the library is to allow certain parts of a pipeline to progress at the same time in the real-world. In the example, they are calls to asyncio.sleep, but in real cases, they could be HTTP requests. As in, the bytes of the request/response will be going across the wire at the same time, so the total wall-clock time of (certain) pipelines will go down.
So, I claim "parallelise" _is_ appropriate.
Like you have a pipeline of [coro1, coro2] where coro2 is using data as it's yielded out of coro1.
But you can also just have like, coro2 yield from coro1 or similar.
I think I like it. Not sure yet. It might help composibility if you find yourself slapping together a lot of discreet little coros.
And for writing to disk, async is basically the only way to "parallelize" such operations
I just think its inappropriate because, if I was looking for a parallelization library like multiprocessing, and I found this, I might waste 15 minutes or so before I figure out that this isn't parallel, it's just async.
Async is okay, but it does nothing to improve performance, like what parallelization does. Async provides good UX, parallelization provides good performance. Conflating the two just muddys the water for the people who like to do high-performance-computing.
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.
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-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
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.
Async re-merges functions that have been shattered due to having to block.
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
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
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
And yes, forget about .then and other old school stuff like that in JS. Just use async await to deal with promises.
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:
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.
From a mental model perspective, I would recommend 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.
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.
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.
While yes, running a coroutine in a new task is in the standard library, that is only a component of what this library does, which is:
- given an async iterable,
- iterating over it in a new task, into a buffer,
- and then returning an iterable that yields values from this buffer as it's filled.
I don't _think_ this logic is in the standard library, at least not at https://docs.python.org/3/library/asyncio-task.html#asyncio.... from what I can see.
Have you thought about making it work as a decorator?
The issue is what happens on exceptions to avoid tasks hanging around forever. Exceptions propagate “forward” in the pipeline fairly automatically. However, getting the ones “behind” the exception to notice: that was tricky. The best I came up with was the bit of state wrapped up in buffered_pipeline that notices the exceptions and cancels the tasks.
I've gotten along ok with create_task and gather. I'll need to spend some time with this implementation to see if it fits any of my existing asyncio coding patterns.
An eagerly executing coroutine to consume the original generator and put it in a queue, and a new simple generator that yields values from the queue till it's told to stop.
I'm not sure if this matches the behavior of the original library for task cancellation though.
[Edit] This would also make a good decorator.What is the mechanism used to parallelize, threads, processes or something else - it's worth mentioning in the readme.
So I started using multiprocessing and multithreading available via concurrent.futures. I built a wrapper package called `ori`[2] that lets you build chains of threadpools and processpools without having to enter the Python asyncio kingdom.
[1]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
[2]: https://ori.technology.neocrym.com/en/latest/ori.poolchain/#...
Ours is based off Twisted, which works pretty similarly to the new async stuff in python3.
The thing ours does, that I can't tell if this one supports or not, is you pass it a list of functions you want it to call (in python3 you would just pass it coroutines I suppose, but in Twisted an async function gets scheduled automatically, so you need to pass it in an uncalled state), and then you specify the "width" of the pipeline, and it runs through your list of calls, ensuring that "width" number of calls are in flight at a time.
It's a super useful little utility to have around.