【问题标题】:Does await always give other tasks a chance to execute?await 是否总是让其他任务有机会执行?
【发布时间】:2020-01-31 00:33:26
【问题描述】:

我想知道当事件循环切换任务时,python 提供了什么保证。

据我了解,async / await 与线程有很大不同,因为事件循环不会根据时间片切换任务,这意味着除非任务产生 (await),否则它将无限期地进行.这实际上很有用,因为在 asyncio 下管理关键部分比使用线程更容易。

我不太清楚的是以下内容:

async def caller():
    while True:
        await callee()


async def callee():
    pass

在此示例中,caller 重复出现 await。所以从技术上讲,它正在屈服。但我不清楚这是否会允许事件循环上的其他任务执行,因为它只屈服于callee,而且永远不会屈服。

也就是说,如果我在“关键部分”内等待callee,即使我知道它不会阻塞,我是否有可能发生其他意外情况?

【问题讨论】:

  • async/await 不做并发,只是协程。事件循环必须决定是否以及如何调度协程。您是否考虑过特定​​的事件循环(asyncio、trio、curio、...)?
  • 这可能会回答你的问题:how does asyncio actually work
  • 协程不能自行停止——它们不能yield 进入事件循环。需要自定义 __await__,这些通常随事件循环一起提供。协程只是创建从事件循环到 yields 的叶子的连接。
  • @MisterMiyagi 从来没有;它是协程负责,不是事件循环。事件循环在何时发生 yield 时没有发言权,如果协程什么都不等待,它确实有一些控制权。您的第一条评论具有误导性,因为这不是关于事件循环行为的问题,而是协程机制的行为。

标签: python async-await


【解决方案1】:

你保持警惕是对的。 caller 产生于 callee,并产生于事件循环。然后事件循环决定恢复哪个任务。其他任务可能(希望)在调用callee 之间进行。 callee 需要等待一个实际阻塞的Awaitableasyncio.Futureasyncio.sleep()不是协程,否则直到caller 返回时控件才会返回到事件循环。

例如,以下代码将在开始处理caller1 任务之前完成caller2 任务。因为callee2本质上是一个同步函数,不需要等待阻塞的I/O操作,因此,不会创建挂起点,caller2会在每次调用callee2后立即恢复。

import asyncio
import time

async def caller1():
    for i in range(5):
        await callee1()

async def callee1():
    await asyncio.sleep(1)
    print(f"called at {time.strftime('%X')}")

async def caller2():
    for i in range(5):
        await callee2()

async def callee2():
    time.sleep(1)
    print(f"sync called at {time.strftime('%X')}")

async def main():
    task1 = asyncio.create_task(caller1())
    task2 = asyncio.create_task(caller2())
    await task1
    await task2

asyncio.run(main())

结果:

sync called at 19:23:39
sync called at 19:23:40
sync called at 19:23:41
sync called at 19:23:42
sync called at 19:23:43
called at 19:23:43
called at 19:23:44
called at 19:23:45
called at 19:23:46
called at 19:23:47

但是如果callee2等待如下,即使等待asyncio.sleep(0)也会发生任务切换,任务会并发运行。

async def callee2():
    await asyncio.sleep(1)
    print('sync called')

结果:

called at 19:22:52
sync called at 19:22:52
called at 19:22:53
sync called at 19:22:53
called at 19:22:54
sync called at 19:22:54
called at 19:22:55
sync called at 19:22:55
called at 19:22:56
sync called at 19:22:56

这种行为不一定是直观的,但考虑到 asyncio 被用于同时处理 I/O 操作和网络,而不是通常的同步 python 代码,它是有意义的。

要注意的另一件事是:如果 callee 等待一个协程,而该协程又在等待一个 asyncio.Futureasyncio.sleep() 或另一个在链中等待其中一个事情的协程,这仍然有效。当等待阻塞Awaitable 时,流控制将返回到事件循环。所以以下也有效。

async def callee2():
    await inner_callee()
    print(f"sync called at {time.strftime('%X')}")

async def inner_callee():
    await asyncio.sleep(1)

【讨论】:

    【解决方案2】:

    TLDR:没有。协程及其各自的关键字(awaitasync withasync for)仅启用暂停。是否发生暂停取决于所使用的框架(如果有的话)。

    第三方异步函数/迭代器/上下文管理器可以充当 检查站;如果您看到await <something> 或它的一位朋友,那么 那可能是一个检查站。所以为了安全起见,你应该做好准备 在那里安排或取消。

    [Trio documentation]


    Python 的await 语法是围绕两个基本机制的语法糖:yield 用于 暂时挂起 值,return 用于 永久退出一个值。这些与生成器函数协程可以使用的相同:

    def gencoroutine():
        for i in range(5):
            yield i  # temporarily suspend
        return 5     # permanently exit
    

    值得注意的是,return 确实暗示暂停。生成器协程可能永远不会yield

    await 关键字(及其兄弟yield from)与yieldreturn 机制相互作用:

    • 如果它的目标yields、await 将暂停“传递”给它自己的调用者。这允许暂停整个堆栈所有await彼此的协程。
    • 如果它的目标returnss,await 捕获返回值并将其提供给它自己的协程。这允许直接将值返回给“调用者”,而无需暂停。

    这意味着await 确实保证会发生暂停。await 的目标来触发暂停。


    就其本身而言,async def 协程只能 return 不暂停,而await 可以允许 暂停。它不能自行挂起(yield 不会挂起到事件循环)。

    async def unyielding():
        return 2  # or `pass`
    

    这意味着只是协程的await从不暂停。只有特定的 awaitables 才能挂起。


    只有使用自定义 __await__ 方法的 awaitables 才能暂停。这些可以yield 直接到事件循环。

    class YieldToLoop:
         def __await__(self):
             yield   # to event loop
             return  # to awaiter
    

    这意味着框架的可等待对象的await 将直接或间接挂起

    挂起的确切语义取决于使用的异步框架。例如,sleep(0) 是否触发暂停,或者运行哪个协程,取决于框架。这也扩展到异步迭代器和上下文管理器——例如,许多异步上下文管理器将在进入或退出时暂停任一,但不会同时暂停。

    Trio

    如果您调用 Trio (await <something in trio>) 提供的异步函数,并且它没有引发异常,则它始终充当检查点。 (如果它确实引发了异常,它可能会充当检查点,也可能不会。)

    Asyncio

    sleep() 总是暂停当前任务,允许其他任务运行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-06-25
      • 1970-01-01
      • 2016-03-13
      • 1970-01-01
      • 2020-03-29
      • 2014-06-10
      • 2015-07-08
      相关资源
      最近更新 更多