如何限制Python异步IO的并发性?
假设我们有一堆链接要下载,每个链接可能需要不同的下载时间。我只允许使用最多3个连接进行下载。现在,我想确保使用asyncio有效地完成这项工作 以下是我试图实现的目标:在任何时间点,尝试确保至少有3次下载正在运行如何限制Python异步IO的并发性?,python,python-3.x,asynchronous,concurrency,python-asyncio,Python,Python 3.x,Asynchronous,Concurrency,Python Asyncio,假设我们有一堆链接要下载,每个链接可能需要不同的下载时间。我只允许使用最多3个连接进行下载。现在,我想确保使用asyncio有效地完成这项工作 以下是我试图实现的目标:在任何时间点,尝试确保至少有3次下载正在运行 Connection 1: 1---------7---9--- Connection 2: 2---4----6----- Connection 3: 3-----5---8----- 数字表示下载链接,连字符表示等待下载 这是我现在正在使用的代码 from random impo
Connection 1: 1---------7---9---
Connection 2: 2---4----6-----
Connection 3: 3-----5---8-----
数字表示下载链接,连字符表示等待下载
这是我现在正在使用的代码
from random import randint
import asyncio
count = 0
async def download(code, permit_download, no_concurrent, downloading_event):
global count
downloading_event.set()
wait_time = randint(1, 3)
print('downloading {} will take {} second(s)'.format(code, wait_time))
await asyncio.sleep(wait_time) # I/O, context will switch to main function
print('downloaded {}'.format(code))
count -= 1
if count < no_concurrent and not permit_download.is_set():
permit_download.set()
async def main(loop):
global count
permit_download = asyncio.Event()
permit_download.set()
downloading_event = asyncio.Event()
no_concurrent = 3
i = 0
while i < 9:
if permit_download.is_set():
count += 1
if count >= no_concurrent:
permit_download.clear()
loop.create_task(download(i, permit_download, no_concurrent, downloading_event))
await downloading_event.wait() # To force context to switch to download function
downloading_event.clear()
i += 1
else:
await permit_download.wait()
await asyncio.sleep(9)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main(loop))
finally:
loop.close()
但我的问题是:
main
功能之前,是否有有效的方法等待最后一次下载完成?(我知道有asyncio.wait
,但我需要存储所有的任务引用以使其工作)
2.什么是处理常见异步模式的好库?(类似)在阅读本答案的其余部分之前,请注意,限制使用asyncio的并行任务数量的惯用方法是使用
asyncio.Semaphore
,如中所示,并在中进行了优雅的抽象。这个答案包含了一些可行的方法,但实现同样目标的方法要复杂一些。我留下答案是因为在某些情况下,这种方法可能比信号量有优势,特别是当要完成的工作非常大或没有边界时,并且您无法提前创建所有的协同路由。在这种情况下,第二个(基于队列的)解决方案是:这个答案就是您想要的。但在大多数常规情况下,例如通过aiohttp并行下载,应该使用信号量
您基本上需要一个固定大小的下载任务池
asyncio
没有预先创建的任务池,但创建一个任务池很容易:只需保留一组任务,不允许其增长超过限制。尽管问题表明您不愿意走这条路,但代码最终要优雅得多:
import asyncio
import random
async def download(code):
wait_time = random.randint(1, 3)
print('downloading {} will take {} second(s)'.format(code, wait_time))
await asyncio.sleep(wait_time) # I/O, context will switch to main function
print('downloaded {}'.format(code))
async def main(loop):
no_concurrent = 3
dltasks = set()
i = 0
while i < 9:
if len(dltasks) >= no_concurrent:
# Wait for some download to finish before adding a new one
_done, dltasks = await asyncio.wait(
dltasks, return_when=asyncio.FIRST_COMPLETED)
dltasks.add(loop.create_task(download(i)))
i += 1
# Wait for the remaining downloads to finish
await asyncio.wait(dltasks)
至于你的另一个问题,显而易见的选择是。如果我没有弄错的话,你在寻找什么。用法示例:
import asyncio
from random import randint
async def download(code):
wait_time = randint(1, 3)
print('downloading {} will take {} second(s)'.format(code, wait_time))
await asyncio.sleep(wait_time) # I/O, context will switch to main function
print('downloaded {}'.format(code))
sem = asyncio.Semaphore(3)
async def safe_download(i):
async with sem: # semaphore limits num of simultaneous downloads
return await download(i)
async def main():
tasks = [
asyncio.ensure_future(safe_download(i)) # creating task starts coroutine
for i
in range(9)
]
await asyncio.gather(*tasks) # await moment all downloads done
if __name__ == '__main__':
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.run_until_complete(loop.shutdown_asyncgens())
loop.close()
输出:
downloading 0 will take 3 second(s)
downloading 1 will take 3 second(s)
downloading 2 will take 1 second(s)
downloaded 2
downloading 3 will take 3 second(s)
downloaded 1
downloaded 0
downloading 4 will take 2 second(s)
downloading 5 will take 1 second(s)
downloaded 5
downloaded 3
downloading 6 will take 3 second(s)
downloading 7 will take 1 second(s)
downloaded 4
downloading 8 will take 2 second(s)
downloaded 7
downloaded 8
downloaded 6
可以找到使用
aiohttp
进行异步下载的示例。异步IO池库完全满足您的需要
URL的列表=(“http://www.google.com", "......")
池=AIOPOL(大小=3)
wait pool.map(您的下载路径、URL列表)
小更新:不再需要创建循环。我调整了下面的代码。只是稍微清理一下
# download(code) is the same
async def main():
no_concurrent = 3
dltasks = set()
for i in range(9):
if len(dltasks) >= no_concurrent:
# Wait for some download to finish before adding a new one
_done, dltasks = await asyncio.wait(dltasks, return_when=asyncio.FIRST_COMPLETED)
dltasks.add(asyncio.create_task(download(i)))
# Wait for the remaining downloads to finish
await asyncio.wait(dltasks)
if __name__ == '__main__':
asyncio.run(main())
我用了米哈伊尔的答案,最后得到了这个小宝石
async def gather_with_concurrency(n, *tasks):
semaphore = asyncio.Semaphore(n)
async def sem_task(task):
async with semaphore:
return await task
return await asyncio.gather(*(sem_task(task) for task in tasks))
您将运行它而不是正常聚集
await gather_with_concurrency(100, *my_coroutines)
使用信号量,您还可以创建一个装饰器来包装函数
import asyncio
from functools import wraps
def request_concurrency_limit_decorator(limit=3):
# Bind the default event loop
sem = asyncio.Semaphore(limit)
def executor(func):
@wraps(func)
async def wrapper(*args, **kwargs):
async with sem:
return await func(*args, **kwargs)
return wrapper
return executor
然后,将decorator添加到origin下载函数中
@request_concurrency_limit_decorator(limit=...)
async def download(...):
...
现在您可以像以前一样调用下载函数,但是使用信号量来限制并发性
await download(...)
应该注意的是,在执行decorator函数时,创建的信号量绑定到默认事件循环,因此您不能调用asyncio.run
来创建新循环。相反,请调用asyncio.get\u event\u loop()。运行…
以使用默认事件循环
是否有一个好的Python异步库来处理常见的异步编程模式?就像著名的JavaScript异步包一样。@Shridharshan根据我的经验,asyncio本身包含您通常需要的所有内容。大体上看一看模块。@MikhailGerasimov调用
asyncio。确保未来()是冗余的,因为async.gather()
仍然在内部调用它()。但是,调用变量tasks
将是“错误的”,因为这些还不是任务。asyncio.Semaphore(3)是否意味着每秒有3个请求?还是有什么不同?@politicalscientist这意味着在任何给定的时间点,同时激活的请求不超过3个。第一种方法非常有效,我不需要事先创建和存储所有任务引用(我使用生成器惰性地加载下载链接)。我不知道asyncio.wait有一个“return\u when”参数。@Shridharshan在第二个解决方案中,您只需创建三个用于提前下载的协同路由,实际的下载链接也可以延迟生成。但这是一个品味的问题——我想我也更喜欢实践中的第一个解决方案。@OrangeDog这实际上是有意的,因为OP的代码使用手动而循环。这个想法是为了使他们现有的代码(保留非传统的习惯用法)适应所需的语义。Sempahore从3.8版开始就被弃用,并将在3.10版中删除。官方警告如下。相反,他们要求使用循环。但是如何使用它,任何人都可以提供任何示例。@Krish因为您没有提供代码或确切的错误消息,所以很难说您指的是什么,但请放心,asyncio.Semaphore
并没有被弃用。不推荐使用并且将被删除的是它的构造函数的循环
参数,您可以忽略它,一切都会正常工作。(这不是特定于信号量的,循环
参数正在删除。)这是一个很好的实用函数,+1。看到函数中的函数,我立刻想到了decorators。我有一个小游戏,你可以用装饰器来实现,或者用固定的信号量值或者动态的;然而,这里的解决方案提供了更大的灵活性。这是一个非常清晰和简短的示例!
@request_concurrency_limit_decorator(limit=...)
async def download(...):
...
await download(...)