Python中的'awaiting `会向事件循环让步吗?

m3eecexj  于 2022-11-28  发布在  Python
关注(0)|答案(2)|浏览(81)

我想知道当我们在异步Python代码中await一个协程时到底会发生什么,例如:

await send_message(string)

(1)send_message被添加到事件循环,并且调用协程放弃对事件循环的控制,或者
(2)我们直接跳到send_message
我读到的大多数explanations都指向(1),因为它们将调用协程描述为 exiting,但我自己的实验表明(2)是这样的:我试图在调用者之后但在被调用者之前运行一个协程,但无法实现这一点。

oyxsuwqo

oyxsuwqo1#

免责声明:由于我自己也在寻找这个问题的答案,因此还有待修正(特别是在细节和正确的术语方面)。然而,下面的研究指出了一个非常决定性的“要点”结论:
正确的OP答案:不,await(本身)* 不 * 向事件循环让步,yield向事件循环让步,因此对于给定的情况:“(2)我们直接跳到send_message“。特别是,某些yield表达式是 * 唯一 * 的点,在底部,异步任务实际上可以切换出来(就确定Python代码执行可以暂停的精确位置而言)。
被证实和证明:1)理论/文档,2)实现代码,3)示例。

根据理论/文件

PEP 492:具有asyncawait语法的协程
虽然PEP不依赖于任何特定的事件循环实现,但它只与使用yield作为调度器信号的协程类型相关,表明协程将等待直到一个事件(如IO)完成。
[await]使用yield from实作[加上验证其参数的额外步骤。]...
任何yield from调用链都以yield结束。这是实现Future的基本机制。因为在内部,协程是一种特殊的生成器,所以每个await都被一个yield挂起,挂起的位置在等待调用链的下面(详细解释请参考PEP 3156)...
协程内部基于生成器,因此它们共享实现。类似于生成器对象,协程有throw()send()close()方法。
现有的基于生成器的协程和这个建议背后的愿景是让用户容易看到代码可能被挂起的位置。
在上下文中,“用户容易看到代码可能被挂起的位置”似乎是指这样一个事实,即在同步代码中,yield是例程中可以“挂起”执行的位置,允许其他代码运行,并且该原理现在完美地扩展到异步环境,其中yield(如果其值未在运行任务 * 内 * 消耗,而是传播到调度程序)是“给调度程序的信号”,以切换出任务。
更简洁地说:在yield上。协程(包括那些使用asyncawait语法的协程)是生成器,因此也是如此。
这不仅仅是一个类比,在实现中(见下文),任务“进入”和“退出”协程的实际机制 * 对于异步世界来说并不是什么新的、神奇的或独特的东西,而只是通过调用coro的<generator>.send()方法。这是(据我理解的文本)PEP 492背后的“愿景”的一部分:asyncawait并没有提供新的代码挂起机制,只是在Python已经深受欢迎且功能强大的生成器上添加了异步代码。
和PEP 3156:* “异步”模块 *
loop.slow_callback_duration attribute控制在报告缓慢回调之前两个 * 屈服点 * 之间允许的最大执行时间[原文强调]。
也就是说,一个不间断的代码段(从异步的Angular 来看)被划分为两个连续的yield点之间的代码段(这些点的值达到了运行的Task级别(通过await/yield from隧道),而没有在其中被消耗)。
还有这个:
调度程序没有公共接口,您可以使用yield from futureyield from task与它交互。
反对:“上面写的是'yield from',但你试图争辩说任务 * 只能 * 在yield本身切换出去!yield fromyield是不同的东西,我的朋友,yield from本身不会挂起代码!”
回答:不是矛盾。PEP是说 * 你 * 通过使用yield from future/task与调度程序交互。但是正如上面在PEP 492中所指出的,任何yield from链(~又名await)最终达到yield(“底龟”)。(参见下文),在一些 Package 器工作之后,yield from future实际上yieldfuture相同,并且yield是另一个任务接管的实际“切换出点”。但是,* 您的代码 * 直接将yieldFuture切换到当前的Task是不正确的,因为您将绕过必要的 Package 器。
已经回答了反对意见,并且注意到了它的实际编码考虑,我希望从上面的引用中得出的观点仍然是:Python异步代码中的一个合适的yield最终 * 是这样一件事 ,它已经以任何其他yield都会做的标准方式暂停了代码执行, 现在进一步 * 使调度程序产生可能的任务切换。

按实现代码

asyncio/futures.py

class Future:
...
    def __await__(self):
        if not self.done():
            self._asyncio_future_blocking = True
            yield self  # This tells Task to wait for completion.
        if not self.done():
            raise RuntimeError("await wasn't used with future")
        return self.result()  # May raise too.

    __iter__ = __await__  # make compatible with 'yield from'.

解释:yield self行告诉正在运行的任务暂时退出,让其他任务运行,在self完成后的某个时间返回到这一行。

asyncio世界中,几乎所有的等待项都是事件循环对所有更高级别的await awaitable表达式保持完全的盲态,直到代码执行逐渐下降到await futureyield from future,然后(如这里所看到的)调用yield self,其产生self,然后被当前协程栈在其下运行的Task“捕获”,从而发信号通知任务休息。
asyncio的上下文中,可能唯一的例外是可能使用 bareyield,例如在asyncio.sleep(0)中。由于sleep函数是本文评论中的一个讨论主题,让我们来看看它。
asyncio/tasks.py

@types.coroutine
def __sleep0():
    """Skip one event loop run cycle.
    This is a private helper for 'asyncio.sleep()', used
    when the 'delay' is set to 0.  It uses a bare 'yield'
    expression (which Task.__step knows how to handle)
    instead of creating a Future object.
    """
    yield

async def sleep(delay, result=None, *, loop=None):
    """Coroutine that completes after a given time (in seconds)."""
    if delay <= 0:
        await __sleep0()
        return result

    if loop is None:
        loop = events.get_running_loop()
    else:
        warnings.warn("The loop argument is deprecated since Python 3.8, "
                      "and scheduled for removal in Python 3.10.",
                      DeprecationWarning, stacklevel=2)

    future = loop.create_future()
    h = loop.call_later(delay,
                        futures._set_result_unless_cancelled,
                        future, result)
    try:
        return await future

    finally:
        h.cancel()

注意:我们在这里有两个有趣的情况下,控制可以转移到调度程序:
(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值前进到下一个。

举例

import asyncio
import types

def task_print(s):
    print(f"{asyncio.current_task().get_name()}: {s}")

async def other_task(s):
    task_print(s)

class AwaitableCls:
    def __await__(self):
        task_print("    'Jumped straight into' another `await`; the act of `await awaitable` *itself* doesn't 'pause' anything")
        yield
        task_print("    We're back to our awaitable object because that other task completed")
        asyncio.create_task(other_task("The event loop gets control when `yield` points (from an iterable coroutine) propagate up to the `current_task` through a suitable chain of `await` or `yield from` statements"))

async def coro():
    task_print("  'Jumped straight into' coro; the `await` keyword itself does nothing to 'pause' the current_task")
    await AwaitableCls()
    task_print("  'Jumped straight back into' coro; we have another pending task, but leaving an `__await__` doesn't 'pause' the task any more than entering the `__await__` does")

@types.coroutine
def iterable_coro(context):
    task_print(f"`{context} iterable_coro`: pre-yield")
    yield None # None or a Future object are the only legitimate yields to the task in asyncio
    task_print(f"`{context} iterable_coro`: post-yield")

async def original_task():
    asyncio.create_task(other_task("Aha, but a (suitably unconsumed) *`yield`* DOES 'pause' the current_task allowing the event scheduler to `_wakeup` another task"))

    task_print("Original task")
    await coro()
    task_print("'Jumped straight out of' coro. Leaving a coro, as with leaving/entering any awaitable, doesn't give control to the event loop")
    res = await iterable_coro("await")
    assert res is None
    asyncio.create_task(other_task("This doesn't run until the very end because the generated None following the creation of this task is consumed by the `for` loop"))
    for y in iterable_coro("for y in"):
        task_print(f"But 'ordinary' `yield` points (those which are consumed by the `current_task` itself) behave as ordinary without relinquishing control at the async/task-level; `y={y}`")
    task_print("Done with original task")

asyncio.get_event_loop().run_until_complete(original_task())

运行python3.8产生
任务-1:原始任务
任务-1:“直接跳进”科罗; await关键字本身不会“暂停”当前任务
任务-1:“直接跳进”另一个await; await awaitable * 本身 * 动作不会“暂停”任何内容
任务-2:啊哈,但是一个(适当的未消耗)* yield * 确实“暂停”当前任务,允许事件调度程序_wakeup另一个任务
Task-1:我们回到了可等待对象,因为另一个任务已完成
任务-1:“直接跳回到”科罗;我们有另一个挂起的任务,但是离开__await__并不比进入__await__“暂停”任务更多
Task-1:“直接跳出”科罗。离开coro,就像离开/进入任何等待对象一样,不会给事件循环给予控制权
任务一:预屈服
任务-3:当yield点(来自一个可迭代的协程)通过一个合适的awaityield from语句链向上传播到current_task时,事件循环获得控制
任务一:后屈服
任务一:预屈服
任务-1:但是“普通的”yield点(那些被current_task自身消耗的点)表现为普通的,而不放弃异步/任务级的控制; y=None
任务-1:后屈服
任务-1:已完成原始任务
Task-4:直到最后才会运行,因为在创建此任务后生成的None被for循环使用
实际上,下面的练习可以帮助我们将async/await的功能与“事件循环”等概念分离。前者 * 有助于 * 后者的良好实现和使用,但是,您可以将asyncawait用作专门语法生成器,而无需 * 任何 *“循环”(无论是asyncio还是其他):

import types # no asyncio, nor any other loop framework

async def f1():
    print(1)
    print(await f2(),'= await f2()')
    return 8

@types.coroutine
def f2():
    print(2)
    print((yield 3),'= yield 3')
    return 7

class F3:
   def __await__(self):
        print(4)
        print((yield 5),'= yield 5')
        print(10)
        return 11

task1 = f1()
task2 = F3().__await__()
""" You could say calls to send() represent our
   "manual task management" in this script.
"""
print(task1.send(None), '= task1.send(None)')
print(task2.send(None), '= task2.send(None)')
try:
    print(task1.send(6), 'try task1.send(6)')
except StopIteration as e:
    print(e.value, '= except task1.send(6)')
try:
    print(task2.send(9), 'try task2.send(9)')
except StopIteration as e:
    print(e.value, '= except task2.send(9)')

产生
1
2
3 = task1.send(无)
4
5 = task2.send(无)
6 =产量3
7 =等待f2()
8 =任务1.send(6)除外
9 =产量5
10
11 =任务2.send(9)除外

tgabmvqs

tgabmvqs2#

是的,await将控制权传递回asyncio eventloop,并允许它调度其他异步函数。
另一种方法是

await asyncio.sleep(0)

相关问题