在传统的 Python 代码中,对于文件的输入操作通常采用以下方式:
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,这在某些情况下可能是理想的。