You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Allow limiting the number of concurrent tasks in asyncio.as_completed #74965
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee=Noneclosed_at=Nonecreated_at=<Date2017-06-27.01:05:22.540>labels= ['type-feature', '3.8', 'expert-asyncio']
title='Allow limiting the number of concurrent tasks in asyncio.as_completed'updated_at=<Date2021-10-13.22:44:14.872>user='https://github.com/andybalaam'
asyncio.as_completed allows us to provide lots of coroutines (or Futures) to schedule, and then deal with the results as soon as they are available, in a loop, or a streaming style.
I propose to allow as_completed to work on very large numbers of coroutines, provided through a generator (rather than a list). In order to make this practical, we need to limit the number of coroutines that are scheduled simultaneously to a reasonable number.
For tasks that open files or sockets, a reasonable number might be 1000 or fewer. For other tasks, a much larger number might be reasonable, but we would still like some limit to prevent us running out of memory.
I suggest adding a "limit" argument to as_completed that limits the number of coroutines that it schedules simultaneously.
For me, the key advantage of as_completed (in the proposed modified form) is that it enables a streaming style that looks quite like synchronous code, but is efficient in terms of memory usage (as you'd expect from a streaming style):
Using my prototype implementation, this runs faster and uses much less memory on my machine when you run it with a limit of 100K instead of 1 million concurrent tasks:
Possibly the most controversial thing about this proposal is the fact that we need to allow passing a generator to as_completed instead of enforcing that it be a list. This is fundamental to allowing the style I outlined above, but it's possible that we can do better than the blanket allowing of all generators that I did.
Third parties are not python core devs responsibility.
I don't aware about existing library with such functionality.
Glin commented on Feb 8, 2019
Glinmannequin
Mannequin
More actions
@andrew Svetlov: I was surprised when you wrote "as_completed() is low-level API.", so I wondered what is high level API and I googled it and first that came was official doc:
Where it's written that it's high level API indeed.
IMHO without limit, all of these functions are quite impractical (as_completed, wait, gather, ...). I have simple to moderately complex scripts and I'm running into problems with it (API servers limiting number of requests per minute, /tmp/ (4GB ramdisk) out of space, memory issues...
Please reconsider adding limit to these functions as it's suppose to be high level API, not low level.
Thanks
aparamon commented on Feb 21, 2019
aparamonmannequin
Mannequin
More actions
Might as_completed() be considered a low-level API, but as of Python 3.7 there are seemingly no ready alternatives to achieve proposed behavior. All of asyncio.gather(), asyncio.wait(), asyncio.as_completed() expect awaitables list of limited size; doing something like https://www.artificialworlds.net/blog/2017/06/12/making-100-million-requests-with-python-aiohttp is not straightforward.
A function that takes iterator/async iterator of tasks and is itself generator/async generator is very much wanted, something in the spirit of (but more efficient?)
----
It is an open question whether such function should yield results in the task submission order. Albeit useful, it's a bit harder to implement and (most importantly) has terrible worst-case memory behavior.
I feel like once you lay out all the requirements: taking futures from an (async) generator, limiting the number of concurrent tasks, getting completed tasks to one consumer "as completed", and an implicit requirement that back pressure from the consumer should be handled (i.e. if whoever's iterating through "async for fut in as_completed(...)" is too slow, then the tasks should pause until it catches up), there are too many moving parts, and this should really be implemented using several tasks.
So a straightforward implementation may look like this:
asyncdefbetter_as_completed(futs, limit):
MAX_DONE_FUTS_HELD=10# or some small numbersem=asyncio.Semaphore(limit)
done_q=asyncio.Queue(MAX_DONE_FUTS_HELD)
asyncdefrun_futs():
asyncforfutinfuts:
awaitsem.acquire()
asyncio.create_task(run_one_fut(fut))
asyncwithsem:
awaitdone_q.put(None)
asyncdefrun_one_fut(fut):
try:
fut=asyncio.ensure_future(fut)
awaitasyncio.wait((fut,))
awaitdone_q.put(fut)
finally:
sem.release()
asyncio.create_task(run_futs())
whileTrue:
next_fut=awaitdone_q.get()
ifnext_futisNone:
returnyieldnext_fut
Add proper handling for cancellation and exceptions and whatnot, and it may become a usable implementation.
And no, I do not feel like this should be added to asyncio.as_completed.
aparamon commented on Feb 25, 2019
aparamonmannequin
Mannequin
More actions
an implicit requirement that back pressure from the consumer should be handled (i.e. if whoever's iterating through "async for fut in as_completed(...)" is too slow, then the tasks should pause until it catches up)
No, I don't think it is required or desired to be handled.
My initial sketch was imprecise: it's better to asynchronously yield task results, not "completed task" futures. This way, no additional buffer needed, all error handling can be consolidated inside igather(), and that's actually more compatible with asyncio.gather().
I.e, instead of
----
yield next_fut
----
use
----
yield await next_fut
----
aparamon commented on Apr 16, 2019
aparamonmannequin
Mannequin
More actions
Hello!
Below is updated implementation containing more consistent error handling.
The main rough edges encountered:
asyncio.Queue alone proved insufficient for precise control of limit, as asyncio.create_task() schedules created Task() immediately and it may start executing before being added to queue (thus leading to limit+1 tasks running). Additional semaphore is used to tackle that.
When exception, other running tasks have to be cancel()ed and then await'ed to ensure all tasks are successfully finished by the time igather exits. Just cancel()ing proved not sufficient.
When exception, unscheduled coroutines have to be wrapped with asyncio.create_task(coro).cancel() to avoid RuntimeWarning "coroutine was never awaited". But maybe there is a more elegant way to suppress this warning for a coroutine?
In my client code I didn't so far encounter "an implicit requirement that back pressure from the consumer should be handled", but it should be possible to implement separately and quite straightforwardly, with the help of asyncio.Queue.
I would like to see this implemented. I run into memory and speed issues when running with 1000+ tasks quite frequently, and have to write my own rate limiters around it.
It doesn't look to me that it is adding a large amount of complexity to as_completed. This was closed in 2017, perhaps it is worth revisiting?
We want to add TaskGroups to asyncio (a similar concept to Trio's nurseries). TaskGroups use the async with statement to clearly define where Tasks are created and at which point they are expected to be completed or destroyed.
asyncio.gather(), asyncio.as_completed(), and few others will be considered legacy APIs after we implement TaskGroups. Implementing rate limiting on top of TaskGroups is easier and more reliable.
I'd really prefer to keep as_completed() and especially gather() as is, as I consider them a bit broken already.
The only thing that prevented us from merging them in 3.8 is that we need to formally define & implement ExceptionGroup (or MultiError) in CPython. I'm going to work on an initial PEP for that this week.
The only thing that prevented us from merging them in 3.8 is that we need
to formally define & implement ExceptionGroup (or MultiError) in CPython.
I'm going to work on an initial PEP for that this week.
Activity
andybalaam commentedon Jun 27, 2017
asyncio.as_completed allows us to provide lots of coroutines (or Futures) to schedule, and then deal with the results as soon as they are available, in a loop, or a streaming style.
I propose to allow as_completed to work on very large numbers of coroutines, provided through a generator (rather than a list). In order to make this practical, we need to limit the number of coroutines that are scheduled simultaneously to a reasonable number.
For tasks that open files or sockets, a reasonable number might be 1000 or fewer. For other tasks, a much larger number might be reasonable, but we would still like some limit to prevent us running out of memory.
I suggest adding a "limit" argument to as_completed that limits the number of coroutines that it schedules simultaneously.
For me, the key advantage of as_completed (in the proposed modified form) is that it enables a streaming style that looks quite like synchronous code, but is efficient in terms of memory usage (as you'd expect from a streaming style):
#!/usr/bin/env python3
Using my prototype implementation, this runs faster and uses much less memory on my machine when you run it with a limit of 100K instead of 1 million concurrent tasks:
I have been working on an implementation and there is some discussion in my blog posts: http://www.artificialworlds.net/blog/2017/06/12/making-100-million-requests-with-python-aiohttp/ and http://www.artificialworlds.net/blog/2017/06/27/adding-a-concurrency-limit-to-pythons-asyncio-as_completed/
Possibly the most controversial thing about this proposal is the fact that we need to allow passing a generator to as_completed instead of enforcing that it be a list. This is fundamental to allowing the style I outlined above, but it's possible that we can do better than the blanket allowing of all generators that I did.
andybalaam commentedon Aug 17, 2017
bump
asvetlov commentedon Dec 20, 2017
as_completed() is low-level API.
Let's not overload it with different parameters.
Anyway
as_completed()uses only asyncio.Future and it's public API likeadd_done_callback()etc.You can master everything what you need without asyncio modification.
Let's close the issue with "wont fix" resolution.
1st1 commentedon Dec 20, 2017
I agree, let's keep as_completed() simple for now. Handling generators+async correctly is hard, so we definitely don't have time for this in 3.7.
andybalaam commentedon Jan 9, 2018
I would argue that this makes as_completed a lot more useful than it is now, so would be worth adding (maybe after 3.7).
But, if this does not go into asyncio, is there another library where it would belong? Or should this be a completely new library?
asvetlov commentedon Jan 11, 2018
Third parties are not python core devs responsibility.
I don't aware about existing library with such functionality.
Glin commentedon Feb 8, 2019
@andrew Svetlov: I was surprised when you wrote "as_completed() is low-level API.", so I wondered what is high level API and I googled it and first that came was official doc:
https://docs.python.org/3/library/asyncio-api-index.html
Where it's written that it's high level API indeed.
IMHO without limit, all of these functions are quite impractical (as_completed, wait, gather, ...). I have simple to moderately complex scripts and I'm running into problems with it (API servers limiting number of requests per minute, /tmp/ (4GB ramdisk) out of space, memory issues...
Please reconsider adding limit to these functions as it's suppose to be high level API, not low level.
Thanks
aparamon commentedon Feb 21, 2019
Might as_completed() be considered a low-level API, but as of Python 3.7 there are seemingly no ready alternatives to achieve proposed behavior. All of asyncio.gather(), asyncio.wait(), asyncio.as_completed() expect awaitables list of limited size; doing something like https://www.artificialworlds.net/blog/2017/06/12/making-100-million-requests-with-python-aiohttp is not straightforward.
A function that takes iterator/async iterator of tasks and is itself generator/async generator is very much wanted, something in the spirit of (but more efficient?)
----
It is an open question whether such function should yield results in the task submission order. Albeit useful, it's a bit harder to implement and (most importantly) has terrible worst-case memory behavior.
See also:
https://bugs.python.org/issue33533
h2non/paco#38
twisteroidambassador commentedon Feb 21, 2019
I feel like once you lay out all the requirements: taking futures from an (async) generator, limiting the number of concurrent tasks, getting completed tasks to one consumer "as completed", and an implicit requirement that back pressure from the consumer should be handled (i.e. if whoever's iterating through "async for fut in as_completed(...)" is too slow, then the tasks should pause until it catches up), there are too many moving parts, and this should really be implemented using several tasks.
So a straightforward implementation may look like this:
Add proper handling for cancellation and exceptions and whatnot, and it may become a usable implementation.
And no, I do not feel like this should be added to asyncio.as_completed.
aparamon commentedon Feb 25, 2019
No, I don't think it is required or desired to be handled.
My initial sketch was imprecise: it's better to asynchronously yield task results, not "completed task" futures. This way, no additional buffer needed, all error handling can be consolidated inside
igather(), and that's actually more compatible withasyncio.gather().I.e, instead of
----
yield next_fut
----
use
----
yield await next_fut
----
aparamon commentedon Apr 16, 2019
Hello!
Below is updated implementation containing more consistent error handling.
The main rough edges encountered:
asyncio.Queue alone proved insufficient for precise control of limit, as asyncio.create_task() schedules created Task() immediately and it may start executing before being added to queue (thus leading to limit+1 tasks running). Additional semaphore is used to tackle that.
When exception, other running tasks have to be cancel()ed and then await'ed to ensure all tasks are successfully finished by the time igather exits. Just cancel()ing proved not sufficient.
When exception, unscheduled coroutines have to be wrapped with asyncio.create_task(coro).cancel() to avoid RuntimeWarning "coroutine was never awaited". But maybe there is a more elegant way to suppress this warning for a coroutine?
In my client code I didn't so far encounter "an implicit requirement that back pressure from the consumer should be handled", but it should be possible to implement separately and quite straightforwardly, with the help of asyncio.Queue.
Shall I go ahead and prepare a PR with docs and tests?
lisroach commentedon Sep 9, 2019
I would like to see this implemented. I run into memory and speed issues when running with 1000+ tasks quite frequently, and have to write my own rate limiters around it.
It doesn't look to me that it is adding a large amount of complexity to as_completed. This was closed in 2017, perhaps it is worth revisiting?
1st1 commentedon Sep 9, 2019
We want to add TaskGroups to asyncio (a similar concept to Trio's nurseries). TaskGroups use the
async withstatement to clearly define where Tasks are created and at which point they are expected to be completed or destroyed.asyncio.gather(), asyncio.as_completed(), and few others will be considered legacy APIs after we implement TaskGroups. Implementing rate limiting on top of TaskGroups is easier and more reliable.
I'd really prefer to keep as_completed() and especially gather() as is, as I consider them a bit broken already.
lisroach commentedon Sep 9, 2019
Sounds good, thanks for the explanation Yury. I look forward to the TaskGroups!
1st1 commentedon Sep 9, 2019
FWIW I've been using TaskGroups in EdgeDB codebase extensively: https://github.com/edgedb/edgedb/blob/master/edb/common/taskgroup.py (you can use the code, it's Apache 2)
The only thing that prevented us from merging them in 3.8 is that we need to formally define & implement ExceptionGroup (or MultiError) in CPython. I'm going to work on an initial PEP for that this week.
lisroach commentedon Sep 9, 2019
Oh nice, I remember talking to you about the MultiError before, it will
help simplify some Mock things. Happy to help out if you want more eyes on
it.
On Mon, Sep 9, 2019 at 3:46 PM Yury Selivanov <[email protected]>
wrote:
1st1 commentedon Sep 9, 2019
Yeah, we'll need to chat about that so I can use Mock requirements in the PEP. :)
kumaraditya303 commentedon Oct 18, 2022
We have taskgroups now, is this still needed?
kumaraditya303 commentedon Oct 28, 2022
Let's keep
as_completedsimple for now, there hasn't been any comments since 2019, if this comes up again we can consider it.