将小型函数转换为协程

时间:2019-04-25 21:01:57

标签: python python-3.x python-asyncio

我感觉我对异步IO的理解存在差距:在较大协程的范围内将小功能包装到协程中是否有好处?事件循环正确吗?好处的程度是否取决于包装的功能是IO还是CPU绑定的?

示例:我有一个协程download(),它是:

  1. 通过aiohttp从HTTP端点下载JSON序列化的字节。
  2. 通过bz2.compress()压缩这些字节-本身不等待
  3. 通过aioboto3将压缩字节写入S3

因此,第1部分和第3部分使用这些库中的预定义协程;默认情况下,第2部分没有。

下沉式示例:

import bz2
import io
import aiohttp
import aioboto3

async def download(endpoint, bucket_name, key):
    async with aiohttp.ClientSession() as session:
        async with session.request("GET", endpoint, raise_for_status=True) as resp:
            raw = await resp.read()  # payload (bytes)
            # Yikes - isn't it bad to throw a synchronous call into the middle
            # of a coroutine?
            comp = bz2.compress(raw)
            async with (
                aioboto3.session.Session()
                .resource('s3')
                .Bucket(bucket_name)
            ) as bucket:
                await bucket.upload_fileobj(io.BytesIO(comp), key)

正如上面的评论所暗示的,我一直以来的理解是,将bz2.compress()之类的同步函数放入协程可能会使其混乱。 (即使bz2.compress()的IO绑定可能比CPU绑定更多。)

那么,这种样板通常有什么好处吗?

async def compress(*args, **kwargs):
    return bz2.compress(*args, **kwargs)

(现在是comp = await compress(raw)中的download()。)

Wa-la,现在这是一个值得期待的协程,因为在本地协程中唯一的return是有效的。有必要使用此工具吗?

对于每个this answer,我听说过以类似的方式随机抛出asyncio.sleep(0)的理由-仅是为了回退到调用协程要中断的事件循环。是这样吗?

2 个答案:

答案 0 :(得分:2)

  

那么,这种样板通常有什么好处吗?

async def compress(*args, **kwargs):
    return bz2.compress(*args, **kwargs)

没有任何好处。与期望相反,添加一个await doesn't guarantee控件将被传递给事件循环-仅在等待的协程实际上挂起时才会发生。由于compress不会等待任何东西,因此它永远不会挂起,因此仅是名称上的协程。

请注意,在协程中添加await asyncio.sleep(0)并不能解决问题。有关更详细的讨论,请参见this answer。如果需要运行阻止功能,请使用run_in_executor

async def compress(*args, **kwargs):
    loop = asyncio.get_event_loop()
    return await loop.run_in_executor(None, lambda: bz2.compress(*args, **kwargs))

答案 1 :(得分:1)

协程允许您同时运行某些内容,而不能并行运行。它们允许单线程合作多任务处理。在两种情况下这是有道理的:

  • 您需要像两个生成器一样,以锁步的方式生成结果。
  • 您希望在其他协程等待I / O时做一些有用的事情。

诸如http请求或磁盘I / O之类的东西将允许其他协程在等待操作完成时运行。

bz2.compress()是同步我想在运行时不会释放GIL but does release GIL这意味着在运行时无法进行有意义的工作。也就是说,尽管其他线程会在调用过程中,其他协程不会运行。

如果您要压缩的数据量很大,以至于运行协程的开销相对较小,则可以使用bz2.BZ2Compressor并向其中合理地输入数据小块(例如128KB),将结果写入流(S3支持流,或者可以使用StringIO),并在压缩块之间使用await asyncio.sleep(0)进行控制。

这将使其他协程也可以与压缩协程同时运行。可能还会在套接字级别并行发生S3异步上传,而协程将处于非活动状态。

顺便说一句,使您的压缩器显式地成为async generator可能是表达相同想法的更简单方法。