事件循环如何解析asyncio中的嵌套协程?

问题描述 投票:0回答:1
async def g(i):
    print("sleeping start", i)
    time.sleep(2)
    print("sleeping end", i)

async def task(i):
    print("start task", i)
    await g(i)
    print("end task", i)

async def f(i):
    print("start", i)
    await task(i)
    print("status update",i)
    print("end", i)

async def main():
    await asyncio.gather(*[f(i) for i in range(5)])

asyncio.run(main())

我有一系列嵌套的协程,有一个入口点,可以通过

asyncio.gather
调用触发所有协程。

  1. 在这些类型的设置中,输出是确定性的,即我们知道运行的先验打印顺序吗?
  2. 当调用嵌套协程时,例如,当调用
    await g(1)
    时,事件循环队列可以改为开始运行
    f(2)
    吗?

最后的跟进,如果我确实希望输出的顺序是:

start 0
start task 0
sleeping start 0 
sleeping end 0
end task 0
status update 0
end 0
...

有什么办法可以确保这一点吗?

我的目标实际上是,当包含任务的协程完成时,即达到

sleeping end i
时,它也会立即到达父协程中的
status update i
。理想情况下,我不希望延迟或事件循环在这两个事件之间执行其他任务。

python asynchronous python-asyncio
1个回答
0
投票

从线程到异步编程的主要变化正是you明确告诉异步循环可以在哪里切换任务。因此,如果您的目标不是在代码中的点“a”和点“b”之间进行任何任务切换或暂停,那么只需不放置任何

await
async for
async with
即可2.

之间的陈述

所以,按顺序回顾一下你的代码。首先:如果您在任何时候在异步代码中使用

time.sleep
,那么您根本就违背了使用异步代码的目的:异步中延迟的任何内容
 都必须是非阻塞的,无论是使用 
await
 表达式,或低级内容的回调。 
sleep
 通常在示例代码中使用,但这仍然有效:如果您使用 
time.sleep(...)
 而不是 
await asyncio.sleep(...),即使您的示例也不会比顺序程序更好。在“真实”代码中,“睡眠”通常会让位于某种类型的 I/O 操作:这些“必须”通过等待的适当异步支持调用来完成。例如: await httpx.get(...)
 而不是 
requests.get(...)

只需重命名函数以获得一些理智,并正确调用睡眠。此外,添加所需的导入语句以使该片段成为一个完全独立的有效示例不会伤害任何人。 ;-)

import asyncio async def first(i): print("sleeping start", i) await asyncio.sleep(2) print("sleeping end", i) async def second(i): print("start task", i) await first(i) print("end task", i) async def third(i): print("start", i) await second(i) print("status update",i) print("end", i) async def main(): await asyncio.gather(*[third(i) for i in range(5)]) asyncio.run(main())
所以;

    在这些类型的设置中,输出是确定性的,即我们 知道运行之前打印的顺序吗?
它可能是确定性的 - 但

你不应该指望它。 (是的,笑) - 因为决定论取决于 asyncio.sleep 的“超时”完全相同,以及调用的顺序。在内部,asyncio.sleep

使用回调模式将“睡眠任务”标记为已完成 - 因此现有的实现应该按照它们创建的顺序进行。但内部实施细节的任何变化都会改变这一点。因此,即使它会按升序打印内容,但如果您的问题依赖于它,请不要相信它。威胁“睡眠结束”步骤未确定,并且接近设计代码时要求的 2 秒时间。即使因为,它取决于异步线程实际上是否可以自由返回到正在等待 
time.sleep 的点 - 只有当任何其他正在运行的任务达到 await
 表达式,以及几毫秒,甚至整秒时,它才会发生,如果存在一些没有 
await
s 的阻塞代码,则可能已通过。
当调用嵌套协程时,例如,当调用await first(1)时,事件循环队列可以开始运行third(2)吗?

  1. 是的。 实际上,当调用协同例程函数时,根本不会执行任何代码。所以如果我这样做:
  2. next_step = first(1)
-> 没有

await

,那就是同步调用,当这个归因完成后,不会执行其他代码。只是当我稍后执行 
await next_step
 时(或者在您的情况下,当您为 
await
 调用放置一个 
gather
 时),事件循环将选择接下来运行的内容,它可能是 
的主体third(2)
 代替 
first(1)
 的主体。尽管当前的实现
按顺序运行,如输出打印输出中所示,但这不在规范中,并且在设计时不应信任。
我的目标实际上是,当包含任务的协程完成时,即到达休眠端 i 时,它也会立即到达父协程中的状态更新 i。理想情况下,我不希望延迟或事件循环在这两个事件之间执行其他任务。

  1. 因此,在这段代码中,在
  2. await asyncio.sleep(2)
解析之后,没有其他

await

first
 函数解析并将程序通量返回到 
second
 中等待的位置,然后再次结束 -不再继续运行
await
,并且在
await
中的
third
之后继续执行,并打印“状态更新”:同时不会运行其他异步代码。
现在,一旦我们将 
sleep

调用修复为异步,代码的输出:

start 0
start task 0
sleeping start 0
start 1
start task 1
sleeping start 1
start 2
start task 2
sleeping start 2
start 3
start task 3
sleeping start 3
start 4
start task 4
sleeping start 4
sleeping end 0
end task 0
status update 0
end 0
sleeping end 1
end task 1
status update 1
end 1
sleeping end 2
end task 2
status update 2
end 2
sleeping end 3
end task 3
status update 3
end 3
sleeping end 4
end task 4
status update 4
end 4

正如我上面所说:虽然步骤是按照你
希望
的顺序执行的,但是你不能依赖它 - 但你可以依赖“睡眠结束N”,“结束任务N”和“状态更新N” “对于任何 N 都是确定性的,没有中断代码。

© www.soinside.com 2019 - 2024. All rights reserved.