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

2017-06-18 13:35:23 +08:00
 NoAnyLove

在 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将执行权转交给了后面的对象,但是并没有转交给作为调度者的消息循环?

8929 次点击
所在节点    Python
7 条回复
shyling
2017-06-18 15:55:48 +08:00
You can only make a coroutine call chain pause with a *generator-based* coroutine.

难道 async function 不是 generator-based 么?
NoAnyLove
2017-06-18 23:12:00 +08:00
@shyling 以`async def`声明的函数算作 native coroutine,不算 generator-based 的协程
shyling
2017-06-19 04:46:19 +08:00
gnaggnoyil
2017-06-19 08:07:12 +08:00
个人理解是因为 yield from 可以 yield 一个 Future,所以配合 event loop 的时候可以用于 sleep.其它的 generator 在 coroutine 切换上下文的之后都是继续执行当前上下文的语句的.

而 Future 只能被 yield from,不能被 await.
NoAnyLove
2017-06-19 13:13:21 +08:00
@shyling accepted 的那个答案,投票为 0,明显是错误的,两者并不是完全没有区别。另一个比较可靠一点的说明在这里<https://stackoverflow.com/questions/40571786/asyncio-coroutine-vs-async-def>

@gnaggnoyil `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`函数中。

感觉还有点混乱。
keakon
2017-06-20 11:36:56 +08:00
这个问题看着挺绕的,但是道理却很简单。

假如要用 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 这层的代码。
NoAnyLove
2017-06-21 10:47:19 +08:00
@keakon 非常干感谢你的回复,我反复读了很多遍,感觉明白了很多。我本来纠结的是功能上为啥需要 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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/369299

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX