为什么只有异步函数才能在异步代码中产生?

Why is it that only asynchronous functions can yield in asynchronous code?

Armin Ronacher 在文章 "I'm not feeling the async pressure" 中做出了以下观察:

In threaded code any function can yield. In async code only async functions can. This means for instance that the writer.write method cannot block.

此观察是参考以下代码示例得出的:

from asyncio import start_server, run

async def on_client_connected(reader, writer):
    while True:
        data = await reader.readline()
        if not data:
            break
        writer.write(data)

async def server():
    srv = await start_server(on_client_connected, '127.0.0.1', 8888)
    async with srv:
        await srv.serve_forever()

run(server())

我不明白这条评论。具体来说:

逐行进行:

In threaded code any function can yield.

机器上的程序运行是根据进程组织的。每个进程可能有一个或多个线程。线程和进程一样,由操作系统调度(并且可以被操作系统中断)。 "yield" 在此上下文中的意思是 "letting other code run"。当工作在多个线程之间拆分时,函数 "yield" 很容易:操作系统挂起一个线程中的代码 运行ning,运行s 一些代码在另一个线程中,挂起,返回,并在第一个线程上进行更多工作,依此类推。这样通过线程之间的切换,实现了并发。

在这个执行模型中,挂起的代码是同步的还是异步的并不重要。代码 中,线程正在 运行 逐行执行,因此同步函数的基本假设---在 运行 之间没有发生任何变化宁一行代码和下一行---不违反。

In async code only async functions can.

"Async code"在此上下文中表示单线程应用程序与多线程应用程序执行相同的工作,只是它通过使用异步函数实现并发 within 一个线程,而不是 不同线程之间拆分工作。在这个执行模型中,你的解释器,而不是操作系统,负责根据需要在函数之间切换以实现并发。

在此执行模型中,在位于异步函数内部的同步函数中间暂停工作是不安全的。这样做意味着 运行 在 运行 同步函数的中间添加一些其他代码,打破同步函数所做的 "line-by-line" 假设。

因此,解释器只会等待暂停同步子函数之间的异步函数的执行,而不会在同步子函数之间。这就是异步代码中的同步函数不能产生的语句的意思:一旦同步函数开始 运行ning,它必须完成。

This means for instance that the writer.write method cannot block.

writer.write方法是同步的,因此,当运行在异步程序中时,是不可中断的。如果此方法要阻塞,它不仅会阻塞 运行ning 内部的异步函数,还会阻塞整个程序。那会很糟糕。 writer.write 通过写入写入缓冲区并立即返回来避免阻塞程序。

严格来说,writer.write 可以屏蔽,只是不可取。

如果您需要在异步函数内部进行阻塞,正确的方法是 await 另一个异步函数。这就是例如await writer.drain() 确实如此。这将异步阻塞:当这个特定函数保持阻塞时,它会正确地屈服于其他可以 运行.

的函数

这里的“Yield”是指cooperative multitasking (albeit within a process rather than among them). In the context of the async/await style of Python programming, asynchronous functions are defined in terms of Python预先存在的生成器支持:如果一个功能块(通常用于I/O),其所有正在执行 awaits 的调用者 挂起(带有一个不可见的 yield/yield from,它确实属于生成器类型)。任何生成器的实际调用都是对其 next 方法;该功能实际上 returns.

每个调用者,直到大多数程序员从未编写过的某种驱动程序,都必须参与这种方法的工作:任何没有挂起的函数都会突然承担责任在等待它调用的函数完成时决定下一步做什么的驱动程序。异步性的这种“传染性”方面被称为 “color”;这可能是有问题的,例如当人们忘记 await 协程调用时 看起来 是正确的,因为它看起来像任何其他调用。 (async/await 语法的存在是为了通过隐式将函数转换为状态机来最大程度地减少并发对程序 结构 的破坏,但这种歧义仍然存在。)这也可能是一件好事:异步函数可以在 await 时被中断,因此直接 推断数据结构的一致性。

同步函数因此不能简单地作为定义的问题产生。限制的含义是用普通(同步)call 调用的函数不能产生:它的调用者不准备处理这样的交互。 (如果它无论如何都会发生什么当然是相同的“被遗忘await”。)这也影响重构:一个函数不能在不改变它所有的情况下变成异步的客户(如果他们还没有的话,也让他们异步)。 (这类似于 all I/O 在 Haskell 中的工作方式,因为它会影响执行任何函数的 type任何。)

请注意 yield 允许的 作为与普通 for 一起使用的普通生成器的角色,即使在异步函数中也是如此,但这只是调用者必须期望与被调用者相同的 协议 的一般事实:如果将增强的生成器(“旧式”协程)与 for 一起使用,它只会得到 None 来自每个 (yield),并且如果 async 函数与 for 一起使用,它会产生可能会在发送 它们 时中断的等待对象None.

与线程或所谓的 stackful 协程或 fibers 的区别在于不需要来自调用者,因为实际的函数调用 不会 return 直到 thread/fiber 恢复。 (在线程的情况下,内核也会选择 when 来恢复它。)从这个意义上说,这些方法更容易使用,但是使用光纤可以“偷偷”暂停到任何由于需要向该函数指定 arguments 以告诉它有关要注册自身的用户空间调度程序的需要,该函数部分受到损害(除非您愿意为此使用全局变量……)。另一方面,线程的 开销 甚至比纤程更高,这在大量 运行.

时很重要