为什么Python中只有基于生成器的协程可以真正暂停执行并强制返回事件循环?

在 Python 核心开发人员 Brett Cannon 的一篇文章《 How the heck does async/await work in Python 3.5?》中提到:

One very key point I want to make about the difference between a generator-based coroutine and an async one is that only generator-based coroutines can actually pause execution and force something to be sent down to the event loop.

之后总结中还有一句:

You can only make a coroutine call chain pause with a generator-based coroutine.

对应的翻译版在这里:《[译] Python 3.5 协程究竟是个啥》,引用如下:

关于基于生成器的协程和 async 定义的协程之间的差异,我想说明的关键点是只有基于生成器的协程可以真正的暂停执行并强制性返回给事件循环。

你只能通过基于生成器的定义来实现协程的暂停。

在我的理解中,基于生成器的协程使用 yield from 语句,async 定义的协程使用 await 语句,虽然两者可以接受的对象不同(具体原文中有详细描述),但是两者的作用应该是一样的啊:都是暂停当前协程的执行,转交出执行权,直到 yield from 或者 await 的对象执行完成后再返回,继续执行后面的语句。

对此,PEP-492 也有提,下面是其中提到的例子:

async def read_data(db):
    data = await db.fetch('SELECT ...')
    ...

await , similarly to yield from , suspends execution of read_data coroutine until db.fetch awaitable completes and returns the result data.

It uses the yield from implementation with an extra step of validating its argument.

也就是说,await应该使用了yield from类似的实现,作用也是暂停当前执行流程。那么,为啥await不能“真正的暂停执行并强制性返回给事件循环”?

一个脑洞:难道是因为await将执行权转交给了后面的对象,但是并没有转交给作为调度者的消息循环?


为什么Python中只有基于生成器的协程可以真正暂停执行并强制返回事件循环?

8 回复

You can only make a coroutine call chain pause with a generator-based coroutine.

难道 async function 不是 generator-based 么?


这个问题问到了Python异步编程的核心机制。简单说,只有生成器协程(async def定义的函数)能真正暂停并让出控制权,是因为它们底层依赖生成器的yield机制。

当你写async def foo(): await bar()时,Python在背后做了几件事:

  1. 把函数编译成一个生成器
  2. await实际上就是yield from的语法糖
  3. 每次await都会让生成器yield一个Future或协程对象给事件循环

关键代码结构是这样的:

import asyncio

async def my_coroutine():
    print("开始执行")
    await asyncio.sleep(1)  # 这里真正暂停了执行
    print("恢复执行")
    return "完成"

# 事件循环大致这样处理协程
async def event_loop_style():
    coro = my_coroutine()
    try:
        # 驱动协程执行
        result = await coro
    except StopIteration as e:
        return e.value

而普通的生成器函数(用yield的)虽然也能暂停,但它们没有与事件循环的集成机制。async/await语法在生成器的基础上增加了:

  • 与事件循环的协议(通过__await__方法)
  • 异常传播的正确处理
  • 取消和超时等异步控制

所以本质上,async/await是专门为异步编程设计的生成器变体,它们知道如何与事件循环通信,在await点暂停时会把控制权交还给事件循环,让其他任务可以执行。

总结:生成器协程通过await点与事件循环协作,实现了真正的暂停和切换。

async def声明的函数算作 native coroutine,不算 generator-based 的协程

个人理解是因为 yield from 可以 yield 一个 Future,所以配合 event loop 的时候可以用于 sleep.其它的 generator 在 coroutine 切换上下文的之后都是继续执行当前上下文的语句的.

而 Future 只能被 yield from,不能被 await.

accepted 的那个答案,投票为 0,明显是错误的,两者并不是完全没有区别。另一个比较可靠一点的说明在这里<https://stackoverflow.com/questions/40571786/asyncio-coroutine-vs-async-def>

asyncio.Future可以用于await语句,它有实现__await__()方法。

我用 pudb 调试《 How the heck does async/await work in Python 3.5?》中最后给出的那个 Example。从代码的运行流程来看,Example 中 sleep 协程的actual = yield wait_until语句有点像 Exception 的感觉,step in 会跳回到waited = await sleep(1),再一次 step in,就会像 Exception 没有被处理继续 propagate 一样,跳到了事件循环run_until_complete函数中。

感觉还有点混乱。

这个问题看着挺绕的,但是道理却很简单。

假如要用 native coroutine 来实现「 pause execution and force something to be sent down to the event loop 」,那么你写的代码大概如下:
async def native_coroutine(): # …
async def f(): await native_coroutine()

再来看 native_coroutine 的函数体,你有如下选择:
1. await 一个 awaitable 对象:这会导致你需要再定义一个 native coroutine,递归回到前面的选择。
2. return 一个值:这会导致代码变成同步的,f 函数会立刻接收到 StopIteration 异常。
3. raise 一个异常:这会导致代码变成同步的,f 函数会立刻接收到异常。
4. yield 一个值:在 Python 3.5 或之前是语法错误;在 3.6 或之后它变成了 asynchronous generator (见 PEP 525 ),也不能用在 await 表达式里(它不是 awaitable,没有定义 await 方法,会抛出 TypeError: object async_generator can’t be used in ‘await’ expression )。

对于选择 1,你当然也可以自定义一个 awaitable:
class Awaitable(object):
----def init(self, count):
--------self.count = count
----def await(self):
--------yield from range(self.count)
这样你就不是用 generator based coroutine 来暂停执行了,但这也不是 native coroutine 了。

可见这完全是语言的限制,因为 yield 后面可以跟任意的值,yield from 可以接任意 generator 对象,而 await 却只能接 awaitable 对象。

所以纠结 native coroutine 为什么不能「暂停执行并强制性返回给事件循环」没多大意义,因为你实际在编写最底层调用的那句代码时,肯定要用 yield 或 yield from。但如果不是编写框架,你基本上只需要写到 await native_coroutine 这层的代码。

非常干感谢你的回复,我反复读了很多遍,感觉明白了很多。我本来纠结的是功能上为啥需要 generator-based coroutine,不过你从逻辑上说明了为啥需要 generator-based coroutine,让我也有种豁然开朗的感觉。

《 How the heck does async/await work in Python 3.5?》也提到如果只是和事件循环打交道,其实不用关心这些细节,因为框架 API 会帮助我们处理这些细节。不过我刚好对为啥不能通过 async 定义的协程来实现 asyncio.sleep()比较好奇,所以想研究一下。

我的感觉上是,最底层的协程,必须要通过yield或者yield from语句,将执行流程转交给处于调用最顶层的事件循环。从调试 step in 跟进的结果来看,await 语句不会捕获yield或者yield from返回的值,这个值会被传递给事件循环,也就是调用coro.send(None)的地方。如果最底层的函数也是通过async def定义的,那么将没有办法把执行流程转移给事件循环。

考虑到“暂停并强制性返回给事件循环”,我想到其实还可以抛出异常,因为异常如果没有捕获就会向上传递,可以被事件循环捕获到。结果试了一下不行,才想起异常抛出后,虽然会向yield或者yield from一样暂停当前执行,但是不会从抛出异常的地方继续自行,Orz

回到顶部