我感觉我对异步IO的理解存在差距:在较大协程的范围内将小功能包装到协程中是否有好处?事件循环正确吗?好处的程度是否取决于包装的功能是IO还是CPU绑定的?
示例:我有一个协程download()
,它是:
aiohttp
从HTTP端点下载JSON序列化的字节。bz2.compress()
压缩这些字节-本身不等待 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)
的理由-仅是为了回退到调用协程要中断的事件循环。是这样吗?
答案 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)
协程允许您同时运行某些内容,而不能并行运行。它们允许单线程合作多任务处理。在两种情况下这是有道理的:
诸如http请求或磁盘I / O之类的东西将允许其他协程在等待操作完成时运行。
bz2.compress()
是同步,我想在运行时不会释放GIL but does release GIL。 这意味着在运行时无法进行有意义的工作。也就是说,尽管其他线程会在调用过程中,其他协程不会运行。
如果您要压缩的数据量很大,以至于运行协程的开销相对较小,则可以使用bz2.BZ2Compressor
并向其中合理地输入数据小块(例如128KB),将结果写入流(S3支持流,或者可以使用StringIO),并在压缩块之间使用await asyncio.sleep(0)
进行控制。
这将使其他协程也可以与压缩协程同时运行。可能还会在套接字级别并行发生S3异步上传,而协程将处于非活动状态。
顺便说一句,使您的压缩器显式地成为async generator可能是表达相同想法的更简单方法。