【问题标题】:asyncio.create_task without awaiting resultasyncio.create_task 不等待结果
【发布时间】:2021-10-14 21:08:31
【问题描述】:

我正在尝试理解 asyncio,特别是 await 关键字。

这是我的代码:

async def sleep_and_print(s: str):

   print(f'{datetime.utcnow()} Executing sleep_and_print({s})')
   await asyncio.sleep(1)
   print(f'{datetime.utcnow()} Completed sleep_and_print({s})')

async def main():

   asyncio.create_task(sleep_and_print(0))
   asyncio.create_task(sleep_and_print(1))
   asyncio.create_task(sleep_and_print(2))

   asyncio.gather(*[sleep_and_print(i) for i in range(3)])

if __name__ == "__main__":

   asyncio.run(main())

输出是:

2021-10-14 10:08:03.242891 Executing sleep_and_print(0)
2021-10-14 10:08:03.242891 Executing sleep_and_print(1)       
2021-10-14 10:08:03.242891 Executing sleep_and_print(2)       
2021-10-14 10:08:03.243896 Executing sleep_and_print(0)       
2021-10-14 10:08:03.243896 Executing sleep_and_print(1)       
2021-10-14 10:08:03.243896 Executing sleep_and_print(2)       
_GatheringFuture exception was never retrieved
future: <_GatheringFuture finished exception=CancelledError()>
concurrent.futures._base.CancelledError

我有两个初步的问题:

  1. 为什么使用create_task() 创建三个任务时的行为与使用gather() 创建的任务不同?我知道gather() 将同时提交任务以执行,而create_task() 任务将单独提交。我不明白为什么gather() 会导致异常。

  2. 为什么这个任务会开始但没有执行完

    asyncio.create_task(sleep_and_print(0))

    而这两个都将完成,即使只等待第二个

    asyncio.create_task(sleep_and_print(0))
    await asyncio.create_task(sleep_and_print(1))
    

【问题讨论】:

    标签: python python-asyncio


    【解决方案1】:

    create_task 是同步的:它只是将任务提交给事件循环并立即返回。

    将 coro 协程包装到一个 Task 中并安排其执行。返回任务对象。

    因此,如果您等待它,您只需等待任务对象,因此await create_task(fn()) 在功能上等同于await fn()

    gather()异步的。您必须等待它,否则您只会得到它返回的 coro,或者更准确地说,是 Futures 对象。循环结束时抛出异常,因为 gather() 从未真正运行过(因为没有等待它)。

    更一般地说,从你的的角度来看,await 的效果是暂停执行,直到结果为“in”,然后继续。从代码的的角度来看,效果是返回到事件循环并停止运行,直到满足等待的条件,此时事件循环可能决定,下一步当某些东西屈服于它时,它就可以选择要运行的任务,回到您的任务并继续运行。 (可能不太好:请记住,asyncio 永远不会在等待完成之前继续,但之后直接继续是最好的努力。因此,如果您 await asyncio.sleep(0.0001) 您最终可能会等待比您更长的时间预期。这在实际硬件上很少出现问题,但它可以使编写异步驱动程序在 micropython 之类的东西上更难一些。)

    create task 是“尽快运行”的成语。 (如果你写过 ISR,你可能会使用类似的策略来避免在中断上下文中运行过多的代码,当你需要对某些事件运行一个适度昂贵的回调时。)请注意,如果你从 create_task >在事件循环中并且不让步(通过等待某些东西)任务将永远不会运行;同样,如果循环结束,它永远不会运行(就像这里一样,因为run 只运行到main 完成)。

    请注意,生成一个 coro 然后稍后等待它是非常有意义的:

    async def my_fn():
       await asyncio.sleep(56)
    
    long_fn = my_fn()
    ...
    await long_fn
    

    你可能会这样做,例如将 coros 作为参数传递给函数,或将它们存储在类属性中。 Coros 和 Python 中的函数一样都是一等公民。

    参考文献

    https://docs.python.org/3/library/asyncio-task.html#asyncio.gather

    (注意使用awaitable来指定返回的对象需要等待)

    附言

    请注意,我在这里使用“yield”是指“将控制权交给调度程序”的协程意义,而不是“发送一些值”的生成器意义。在 python 中有一段时间我们将这两者混合在一起,但幸运的是,基于生成器的协同程序的时代已经结束。它奏效了,但很快就变得非常混乱。

    【讨论】:

    • 谢谢。一些后续,因为我显然误解了:> 你可以等待 create_task,但它没有区别它似乎确实有区别。如果我等待它,则任务完成。如果我不等待它,程序会在任务完成之前终止。 > 请注意,如果您不屈服(通过等待某些东西),任务将永远不会运行这与我所看到的相反。任务中没有任何等待语句,它就完成了。
    • @paxtell 在这两种情况下,我写的内容都极具误导性:我已经澄清了。在第一种情况下,我实际上是错了,因为我忘记了create_task 返回了任务对象。我在第二种情况下的评论旨在在循环内创建任务以在我们不关心它何时运行时卸载执行,而不是在开始循环之前创建任务。我想我在嵌入式环境中花费了太多时间,在启动几秒钟后一切都是异步的......
    猜你喜欢
    • 1970-01-01
    • 2017-04-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-22
    • 1970-01-01
    相关资源
    最近更新 更多