为什么不能直接等待python协程对象



所以我正在运行一个异步示例:

import asyncio, time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
# Wait until both tasks are completed (should take
# around 2 seconds.)
await task1
await task2
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())

这段代码的输出正确:

started at 14:36:06
hello
world
finished at 14:36:08

这两个协同程序是异步运行的,最终花了2秒钟,这没有问题。然而,当我将行组合在一起并直接等待Task对象时,如下所示:

import asyncio, time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
# Wait until both tasks are completed (should take
# around 2 seconds.)
await asyncio.create_task(say_after(1, 'hello'))
await asyncio.create_task(say_after(2, 'world'))
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())

这个结果变成:

started at 14:37:12
hello
world
finished at 14:37:15

这花费了3秒,表明协同程序运行不正确。

如何使后一个代码正常工作?或者是什么原因导致了这种差异?

附言:这个例子实际上来自python文档:https://docs.python.org/3.8/library/asyncio-task.html#coroutines

await使代码在等待的协程完成后"停止"并继续,因此当您编写时

await asyncio.create_task(say_after(1, 'hello'))
await asyncio.create_task(say_after(2, 'world'))

第二个任务是在第一个协程完成后创建并运行,因此总共需要3秒。作为解决方案,可以考虑使用类似gatherwait的函数。例如:

import asyncio, time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
# Wait until both tasks are completed (should take
# around 2 seconds.)
await asyncio.gather(say_after(1, 'hello'), say_after(2, 'world'))
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())

输出:

started at 08:10:04
hello
world
finished at 08:10:06

来自文档Await表达式:

挂起对不可用对象执行协程。只能是在协程函数内部使用。

无论何时await,例程都将挂起,直到等待的任务完成。在第一个例子中,两个协程都开始,第二个协程中的2秒睡眠与第一个协程重叠。当您在第一个await之后开始运行时,第二个计时器中已经过了1秒。

在第二个示例中,第二个await asyncio.create_task(say_after(2, 'world'))直到第一个完成并且main继续运行之后才被调度。这时,第二项任务的2秒睡眠开始了。

我结合了这些例子来展示进展。与原始打印不同,我在say_after等待之前打印一条开始消息,在main的await之后打印一条结束消息。你可以在结果中看到时差。

import asyncio, time
async def say_after(delay, what):
print(f"start {what} at {time.strftime('%X')}")
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
await task1
print(f"Finished hello at {time.strftime('%X')}")
await task2
print(f"Finished world at {time.strftime('%X')}")
async def main2():
await asyncio.create_task(say_after(1, 'hello'))
print(f"Finished hello at {time.strftime('%X')}")
await asyncio.create_task(say_after(2, 'world'))
print(f"Finished world at {time.strftime('%X')}")
print("========== Test 1 ============")
asyncio.run(main())
print("========== Test 2 ============")
asyncio.run(main2())

第二次测试的结果表明,在第一次测试完成之前,不会调用第二个say_after

========== Test 1 ============
start hello at 00:51:42
start world at 00:51:42
hello
Finished hello at 00:51:43
world
Finished world at 00:51:44
========== Test 2 ============
start hello at 00:51:44
hello
Finished hello at 00:51:45
start world at 00:51:45
world
Finished world at 00:51:47

main中,创建任务以运行asyncio.sleep,但直到main返回偶数循环,这些任务才真正运行。如果我们添加一个time.sleep(3),我们可能会期望这两个重叠的异步睡眠已经完成,但事实上,say_after甚至直到第一个允许事件循环继续的await才运行。

import asyncio, time
async def say_after(delay, what):
print(f"starting {what} at {time.time()-start}")
await asyncio.sleep(delay)
print(what)
async def main():
global start
print('time asyncio.sleep with intermedite time.sleep')
start = time.time()
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
# similate working for 3 seconds with non asyncio sleep
time.sleep(3)
print(f'expect 3 got {time.time()-start}')
await task1  # <== where the 2 `say_after` tasks start
print(f'expect 3 got {time.time()-start}')
await task2
print(f'expect 3 got {time.time()-start}')
asyncio.run(main())

生成

time asyncio.sleep with intermedite time.sleep
expect 3 got 3.0034446716308594
starting hello at 3.003699541091919
starting world at 3.0038907527923584
hello
expect 3 got 4.005880355834961
world
expect 3 got 5.00671124458313

在设置任务后将asyncio.sleep(0)添加到main中,可以让它们运行并进行自己的重叠睡眠,代码也可以按照我们的要求工作。

import asyncio, time
async def say_after(delay, what):
print(f"starting {what} at {time.time()-start}")
await asyncio.sleep(delay)
print(what)
async def main():
global start
print('time asyncio.sleep with event loop poll and intermedite time.sleep')
start = time.time()
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
# let the `say_after` tasks (and anything else pending) run
await asyncio.sleep(0)
# similate working for 3 seconds with non asyncio sleep
time.sleep(3)
print(f'expect 3 got {time.time()-start}')
await task1  # <== where the 2 `say_after` tasks start
print(f'expect 3 got {time.time()-start}')
await task2
print(f'expect 3 got {time.time()-start}')
asyncio.run(main())

我现在有点理解这个问题了。。。

wait使进程在该行被阻塞。

所以在主功能中,如果你想做parrelle任务,最好使用asyncio.wait/collect…

我认为正是Asyncio的设计风格使以前的代码工作得很好。。。

最新更新