首页 > 解决方案 > 将小函数转换为协程

问题描述

我觉得我对异步 IO 的理解存在差距:在更大的协程范围内将小函数包装到协程中是否有好处? 正确地发出事件循环的信号是否有好处?这种好处的程度是否取决于包装的函数是 IO 还是 CPU 绑定的?

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

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

所以第 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().)

哇啦,这现在是一个可等待的协程,因为一个单一return的在原生协程中是有效的。有没有理由使用它?

根据这个答案,我听说过asyncio.sleep(0)以类似方式随机投入的理由 - 只是为了单一备份到调用协程想要中断的事件循环。这是正确的吗?

标签: pythonpython-3.xpython-asyncio

解决方案


那么,这种类型的样板文件通常有什么好处吗?

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

对它没有任何好处。与预期相反,添加 anawait 并不能保证控件将传递给事件循环 - 只有在等待的协程实际挂起时才会发生。由于compress不等待任何东西,它永远不会挂起,所以它只是名义上的协程。

注意加入await asyncio.sleep(0)协程并不能解决问题;有关更详细的讨论,请参见此答案。如果您需要运行阻塞功能,请使用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))

推荐阅读