在 asyncio 中使用文件处理代码

67 阅读3分钟

在传统的 Python 代码中,对于文件的输入操作通常采用以下方式:

huake_00210_.jpg

def foo(file_obj):
    data = file_obj.read()
    # 接下来进行其他操作

客户端代码负责打开文件,查找指定位置(如需要),然后关闭文件。如果客户端想向我们传递管道或套接字(甚至 StringIO),这样的操作是可行的。

然而,这种方式与 asyncio 不兼容,asyncio 需要类似于以下的语法:

def foo(file_obj):
    data = yield from file_obj.read()
    # 接下来进行其他操作

显然,这种语法只适用于 asyncio 对象;尝试将其用于传统的文件对象会引起混乱。反之亦然。

更糟的是,我认为目前还没有办法在传统 .read() 方法中包裹 yield from,因为我们需要一直向事件循环传递数据,而不仅仅是读取发生时的情况。gevent 库确实做到了这一点,但我不知道如何将他们的协程代码调整为生成器。

如果我编写一个处理文件输入的库,我该如何处理这种情况?我需要有两个版本的 foo() 函数吗?我有许多这样的函数;重复所有这些函数是不可扩展的。

我可能会告诉客户端开发人员使用 run_in_executor() 或一些等价的代码,但感觉这就像对抗 asyncio 而不是与之配合。

2、解决方案

答案1:

这是显式异步框架的一个缺点。与 gevent 不同,gevent 可以对同步代码进行补丁,使其在没有任何代码更改的情况下变得异步,但如果不使用 asyncio.coroutine 和 yield from(或者至少使用 asyncio.Futures 和回调)重新编写代码,就无法使同步代码与 asyncio 兼容。

据我所知,无法让同一个函数在 asyncio 和普通同步上下文中都正常工作;任何与 asyncio 兼容的代码都将依赖正在运行的事件循环来驱动异步部分,因此它在普通上下文中无法工作,而同步代码如果在 asyncio 上下文中运行,总是会阻塞事件循环。这就是为什么你通常会看到库的 asyncio 特定版本(或至少是异步框架特定版本)与同步版本并存的原因。根本没有好的方法来提供一个与两者都兼容的统一 API。

答案2:

经过进一步思考,我认为这是有可能的,但并不十分完美。

从 foo() 的传统版本开始:

def foo(file_obj):
    data = file_obj.read()
    # 接下来进行其他操作

我们需要传递一个在此处“正确”运行的文件对象。当文件对象需要执行 I/O 时,它应该遵循以下流程:

  • 它创建一个新的事件。
  • 它创建一个闭包,在调用时执行必要的 I/O,然后设置事件。
  • 它使用 call_soon_threadsafe() 将闭包交给事件循环。
  • 它阻塞事件。

以下是一些示例代码:

import asyncio, threading

# 在 file 对象类中
def read(self):
    event = threading.Event()
    def closure():
        # self.reader 是一个 asyncio StreamReader 或类似对象
        self._tmp = yield from self.reader.read()
        event.set()
    asyncio.get_event_loop().call_soon_threadsafe(closure)
    event.wait()
    return self._tmp

然后我们安排在执行器中运行 foo(file_obj)(例如使用 OP 中建议的 run_in_executor())。

这种方法的优点在于,即使 foo() 的作者不了解 asyncio,它也能正常工作。它还确保在事件循环中执行 I/O,这在某些情况下可能是理想的。