我想知道当我们在异步Python代码中await
一个协程时到底会发生什么,例如:
await send_message(string)
(1)send_message
被添加到事件循环,并且调用协程放弃对事件循环的控制,或者
(2)我们直接跳到send_message
我读到的大多数explanations都指向(1),因为它们将调用协程描述为 exiting,但我自己的实验表明(2)是这样的:我试图在调用者之后但在被调用者之前运行一个协程,但无法实现这一点。
我想知道当我们在异步Python代码中await
一个协程时到底会发生什么,例如:
await send_message(string)
(1)send_message
被添加到事件循环,并且调用协程放弃对事件循环的控制,或者
(2)我们直接跳到send_message
我读到的大多数explanations都指向(1),因为它们将调用协程描述为 exiting,但我自己的实验表明(2)是这样的:我试图在调用者之后但在被调用者之前运行一个协程,但无法实现这一点。
2条答案
按热度按时间oyxsuwqo1#
免责声明:由于我自己也在寻找这个问题的答案,因此还有待修正(特别是在细节和正确的术语方面)。然而,下面的研究指出了一个非常决定性的“要点”结论:
正确的OP答案:不,
await
(本身)* 不 * 向事件循环让步,yield
向事件循环让步,因此对于给定的情况:“(2)我们直接跳到send_message
“。特别是,某些yield
表达式是 * 唯一 * 的点,在底部,异步任务实际上可以切换出来(就确定Python代码执行可以暂停的精确位置而言)。被证实和证明:1)理论/文档,2)实现代码,3)示例。
根据理论/文件
PEP 492:具有
async
和await
语法的协程虽然PEP不依赖于任何特定的事件循环实现,但它只与使用
yield
作为调度器信号的协程类型相关,表明协程将等待直到一个事件(如IO)完成。[
await
]使用yield from
实作[加上验证其参数的额外步骤。]...任何
yield from
调用链都以yield
结束。这是实现Future
的基本机制。因为在内部,协程是一种特殊的生成器,所以每个await
都被一个yield
挂起,挂起的位置在等待调用链的下面(详细解释请参考PEP 3156)...协程内部基于生成器,因此它们共享实现。类似于生成器对象,协程有
throw()
,send()
和close()
方法。现有的基于生成器的协程和这个建议背后的愿景是让用户容易看到代码可能被挂起的位置。
在上下文中,“用户容易看到代码可能被挂起的位置”似乎是指这样一个事实,即在同步代码中,
yield
是例程中可以“挂起”执行的位置,允许其他代码运行,并且该原理现在完美地扩展到异步环境,其中yield
(如果其值未在运行任务 * 内 * 消耗,而是传播到调度程序)是“给调度程序的信号”,以切换出任务。更简洁地说:在
yield
上。协程(包括那些使用async
和await
语法的协程)是生成器,因此也是如此。这不仅仅是一个类比,在实现中(见下文),任务“进入”和“退出”协程的实际机制 * 对于异步世界来说并不是什么新的、神奇的或独特的东西,而只是通过调用coro的
<generator>.send()
方法。这是(据我理解的文本)PEP 492背后的“愿景”的一部分:async
和await
并没有提供新的代码挂起机制,只是在Python已经深受欢迎且功能强大的生成器上添加了异步代码。和PEP 3156:* “异步”模块 *
loop.slow_callback_duration attribute
控制在报告缓慢回调之前两个 * 屈服点 * 之间允许的最大执行时间[原文强调]。也就是说,一个不间断的代码段(从异步的Angular 来看)被划分为两个连续的
yield
点之间的代码段(这些点的值达到了运行的Task
级别(通过await
/yield from
隧道),而没有在其中被消耗)。还有这个:
调度程序没有公共接口,您可以使用
yield from future
和yield from task
与它交互。反对:“上面写的是'
yield from
',但你试图争辩说任务 * 只能 * 在yield
本身切换出去!yield from
和yield
是不同的东西,我的朋友,yield from
本身不会挂起代码!”回答:不是矛盾。PEP是说 * 你 * 通过使用
yield from future/task
与调度程序交互。但是正如上面在PEP 492中所指出的,任何yield from
链(~又名await
)最终达到yield
(“底龟”)。(参见下文),在一些 Package 器工作之后,yield from future
实际上yield
与future
相同,并且yield
是另一个任务接管的实际“切换出点”。但是,* 您的代码 * 直接将yield
从Future
切换到当前的Task
是不正确的,因为您将绕过必要的 Package 器。已经回答了反对意见,并且注意到了它的实际编码考虑,我希望从上面的引用中得出的观点仍然是:Python异步代码中的一个合适的
yield
最终 * 是这样一件事 ,它已经以任何其他yield
都会做的标准方式暂停了代码执行, 现在进一步 * 使调度程序产生可能的任务切换。按实现代码
asyncio/futures.py
解释:
yield self
行告诉正在运行的任务暂时退出,让其他任务运行,在self
完成后的某个时间返回到这一行。在
asyncio
世界中,几乎所有的等待项都是事件循环对所有更高级别的await awaitable
表达式保持完全的盲态,直到代码执行逐渐下降到await future
或yield from future
,然后(如这里所看到的)调用yield self
,其产生self
,然后被当前协程栈在其下运行的Task
“捕获”,从而发信号通知任务休息。在
asyncio
的上下文中,可能唯一的例外是可能使用 bareyield
,例如在asyncio.sleep(0)
中。由于sleep
函数是本文评论中的一个讨论主题,让我们来看看它。asyncio/tasks.py
注意:我们在这里有两个有趣的情况下,控制可以转移到调度程序:
(1)
__sleep0
中的裸yield
(当通过await
调用时)。(2)把
yield self
紧接在await future
之内。asyncio/tasks.py中的关键行(对于我们的目的)是当
Task._step
通过result = self._coro.send(None)
运行它的顶级协程并识别fourish case时:(1)
result = None
由科罗(其再次是生成器)生成:任务“放弃对一个事件循环迭代的控制”。(2)
result = future
在科罗内生成,具有进一步的魔术成员字段证据,该魔术成员字段证据表明未来是以适当的方式从Future.__iter__ == Future.__await__
中产生的:任务放弃对事件循环的控制直到将来完成。(3)
StopIteration
由科罗引发,表示协程完成(即作为生成器,它耗尽了它所有的yield
):任务的最终结果(其本身是X1 M78 N1 X)被设置为协程返回值。(4)发生任何其他
Exception
:相应地设置任务set_exception
。模细节,我们关注的主要点是
asyncio
事件循环中的协程段最终通过coro.send()
运行。除了初始启动和最终终止,send()
精确地从它生成的最后一个yield
值前进到下一个。举例
运行python3.8产生
任务-1:原始任务
任务-1:“直接跳进”科罗;
await
关键字本身不会“暂停”当前任务任务-1:“直接跳进”另一个
await
;await awaitable
* 本身 * 动作不会“暂停”任何内容任务-2:啊哈,但是一个(适当的未消耗)*
yield
* 确实“暂停”当前任务,允许事件调度程序_wakeup
另一个任务Task-1:我们回到了可等待对象,因为另一个任务已完成
任务-1:“直接跳回到”科罗;我们有另一个挂起的任务,但是离开
__await__
并不比进入__await__
“暂停”任务更多Task-1:“直接跳出”科罗。离开coro,就像离开/进入任何等待对象一样,不会给事件循环给予控制权
任务一:预屈服
任务-3:当
yield
点(来自一个可迭代的协程)通过一个合适的await
或yield from
语句链向上传播到current_task
时,事件循环获得控制任务一:后屈服
任务一:预屈服
任务-1:但是“普通的”
yield
点(那些被current_task
自身消耗的点)表现为普通的,而不放弃异步/任务级的控制;y=None
个任务-1:后屈服
任务-1:已完成原始任务
Task-4:直到最后才会运行,因为在创建此任务后生成的None被
for
循环使用实际上,下面的练习可以帮助我们将
async
/await
的功能与“事件循环”等概念分离。前者 * 有助于 * 后者的良好实现和使用,但是,您可以将async
和await
用作专门语法生成器,而无需 * 任何 *“循环”(无论是asyncio
还是其他):产生
1
2
3 = task1.send(无)
4
5 = task2.send(无)
6 =产量3
7 =等待f2()
8 =任务1.send(6)除外
9 =产量5
10
11 =任务2.send(9)除外
tgabmvqs2#
是的,await将控制权传递回asyncio eventloop,并允许它调度其他异步函数。
另一种方法是