【问题标题】:Why you cannot await a python coroutine object directly?为什么不能直接等待 python 协程对象?
【发布时间】:2020-12-11 06:49:21
【问题描述】:

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

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个协程异步运行,最后花了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秒,说明协程运行不正确。

如何使后面的代码正常工作?还是有什么 idk 导致了这种差异?

附:该示例实际上来自python doc:https://docs.python.org/3.8/library/asyncio-task.html#coroutines

【问题讨论】:

  • await asyncio.create_task(say_after(1, 'hello')) 的奇怪之处在于您创建了一个任务对象,然后立即等待它。它与await say_after(1, 'hello') 相同。
  • This answer 也详细介绍了这个问题。

标签: python python-3.x asynchronous python-asyncio


【解决方案1】:

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

【讨论】:

  • 是的,我知道我可以使用 asyncio.gather / asyncio.wait,协程可以完美运行...
  • 但是,正如您所解释的the second task is created and run after the first coroutine was completed,为什么以前的代码可以正常工作?
  • 这就是await语句的设计方式,你的第一个例子也很好。只需确保在等待它们之前创建任务,因为create_task 允许它们被安排。
【解决方案2】:

来自文档Await expression

在等待对象上暂停协程的执行。只能是 在协程函数中使用。

每当您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())

【讨论】:

  • 嘿,谢谢你的努力,但我认为你在By the time you start running after the first await, 1 second has already elapsed 上是对的!为了证明这一点,请尝试在task2 = asyncio.create_task(say_after(2, 'world')) 之后添加一行time.sleep(3)。所以如果你是对的,这两个协程应该同时完成。然而事实并非如此!
  • @YihuaZhou time.sleep() 混淆了这个问题,因为它停止了整个事件循环,因此停止了所有协程。在使用或测试 asyncio 时,您应该永远不要调用time.sleep()await asyncio.sleep() 代替。
  • @user4815162342 - 我明白你的意思,但我尝试了sleep(3),它确实增加了 3 秒的延迟,我觉得这很令人费解。 asyncio.sleep 所做的只是在未来安排一个非常无聊的完成例程。 time.sleep(3) 代表任何消耗 CPU 的东西——比如 pandas 转换。我无法弄清楚为什么 asyncio.sleep 会延迟,直到调度协程执行某些操作以返回事件循环。
  • 我不确定我是否理解您在哪里添加了 sleep(3),这增加了 3 秒的延迟,您能说得更具体些吗?另外,sleep(3) 是指您使用了await asyncio.sleep(3) 还是您使用了time.sleep(3)?对于 pandas 等,如果你需要从 asyncio 调用 CPU-bound 代码,你应该使用run_in_executor() 以避免事件循环被攻击。
  • @user4815162342 在第一个示例中,我在 await task1 之前添加了 time.sleep(3)asyncio.sleep 创建一个未来并使用时间增量执行 call_later。我认为超时应该从那时开始倒计时,但似乎要等到await task1。看来我有一些学习要做。
【解决方案3】:

我现在有点明白这个问题了......

await 使进程在该行阻塞。

所以在main函数中,如果你想做并行任务,最好使用asyncio.wait/gather...

我认为只是 Asyncio 的设计风格使之前的代码运行良好......

【讨论】:

    猜你喜欢
    • 2020-01-02
    • 1970-01-01
    • 1970-01-01
    • 2013-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多