如何限制Python异步IO的并发性?

如何限制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

假设我们有一堆链接要下载,每个链接可能需要不同的下载时间。我只允许使用最多3个连接进行下载。现在,我想确保使用asyncio有效地完成这项工作

以下是我试图实现的目标:在任何时间点,尝试确保至少有3次下载正在运行

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()
但我的问题是:

  • 目前,我只需等待9秒钟,以保持主功能运行,直到下载完成。在退出
    main
    功能之前,是否有有效的方法等待最后一次下载完成?(我知道有
    asyncio.wait
    ,但我需要存储所有的任务引用以使其工作)

  • 什么样的图书馆能完成这种任务?我知道javascript有很多异步库,但是Python呢

  • 编辑:
    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(...)